reader-workbench 1.0.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.
- reader_workbench/__init__.py +22 -0
- reader_workbench/__main__.py +4 -0
- reader_workbench/_version.py +17 -0
- reader_workbench/api/__init__.py +74 -0
- reader_workbench/api/_record_reads.py +75 -0
- reader_workbench/api/artifacts.py +79 -0
- reader_workbench/api/facade.py +538 -0
- reader_workbench/api/models.py +285 -0
- reader_workbench/api/notebooks.py +63 -0
- reader_workbench/contracts/__init__.py +18 -0
- reader_workbench/contracts/builtins/__init__.py +36 -0
- reader_workbench/contracts/builtins/cytometry.py +140 -0
- reader_workbench/contracts/builtins/four_state_event_window.py +214 -0
- reader_workbench/contracts/builtins/generic.py +21 -0
- reader_workbench/contracts/builtins/logic.py +149 -0
- reader_workbench/contracts/builtins/plate_reader.py +47 -0
- reader_workbench/contracts/catalog.py +257 -0
- reader_workbench/contracts/model.py +109 -0
- reader_workbench/domains/__init__.py +1 -0
- reader_workbench/domains/cytometry/__init__.py +3 -0
- reader_workbench/domains/cytometry/analysis/__init__.py +29 -0
- reader_workbench/domains/cytometry/analysis/events.py +182 -0
- reader_workbench/domains/cytometry/analysis/gating.py +175 -0
- reader_workbench/domains/cytometry/analysis/workflow.py +248 -0
- reader_workbench/domains/cytometry/io/__init__.py +3 -0
- reader_workbench/domains/cytometry/io/fcs.py +135 -0
- reader_workbench/domains/cytometry/plots/__init__.py +5 -0
- reader_workbench/domains/cytometry/plots/diagnostic.py +155 -0
- reader_workbench/domains/logic/__init__.py +3 -0
- reader_workbench/domains/logic/crosstalk/__init__.py +3 -0
- reader_workbench/domains/logic/crosstalk/pairs.py +661 -0
- reader_workbench/domains/logic/four_state_vector/__init__.py +7 -0
- reader_workbench/domains/logic/four_state_vector/builder.py +321 -0
- reader_workbench/domains/logic/four_state_vector/collection/__init__.py +20 -0
- reader_workbench/domains/logic/four_state_vector/collection/checks.py +49 -0
- reader_workbench/domains/logic/four_state_vector/collection/constants.py +29 -0
- reader_workbench/domains/logic/four_state_vector/collection/model.py +19 -0
- reader_workbench/domains/logic/four_state_vector/collection/render.py +383 -0
- reader_workbench/domains/logic/four_state_vector/collection/sources.py +185 -0
- reader_workbench/domains/logic/four_state_vector/config.py +214 -0
- reader_workbench/domains/logic/four_state_vector/diagnostic.py +361 -0
- reader_workbench/domains/logic/four_state_vector/heatmap.py +86 -0
- reader_workbench/domains/logic/four_state_vector/math.py +191 -0
- reader_workbench/domains/logic/four_state_vector/reference.py +85 -0
- reader_workbench/domains/logic/four_state_vector/selection.py +228 -0
- reader_workbench/domains/logic/four_state_vector/treatment_semantics.py +51 -0
- reader_workbench/domains/logic/four_state_vector/validation.py +19 -0
- reader_workbench/domains/logic/logic_symmetry/__init__.py +3 -0
- reader_workbench/domains/logic/logic_symmetry/encodings.py +93 -0
- reader_workbench/domains/logic/logic_symmetry/extract_corners.py +192 -0
- reader_workbench/domains/logic/logic_symmetry/main.py +236 -0
- reader_workbench/domains/logic/logic_symmetry/metrics.py +96 -0
- reader_workbench/domains/logic/logic_symmetry/overlay.py +129 -0
- reader_workbench/domains/logic/logic_symmetry/prep.py +138 -0
- reader_workbench/domains/logic/logic_symmetry/render.py +356 -0
- reader_workbench/domains/logic/treatment_columns.py +42 -0
- reader_workbench/domains/plate_reader/__init__.py +1 -0
- reader_workbench/domains/plate_reader/analysis/__init__.py +14 -0
- reader_workbench/domains/plate_reader/analysis/fold_change.py +474 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/__init__.py +21 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/aggregation.py +191 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/contract_fields.py +50 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/contracts.py +320 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/design_dispositions.py +54 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/disposition_records.py +140 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/event_sensitivity.py +27 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/materialize.py +274 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/observation_resampling.py +97 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/reduction.py +62 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/seeds.py +15 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/sources.py +308 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/well_exclusion_validation.py +94 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/well_exclusions.py +54 -0
- reader_workbench/domains/plate_reader/analysis/timepoints.py +76 -0
- reader_workbench/domains/plate_reader/io/__init__.py +6 -0
- reader_workbench/domains/plate_reader/io/sample_map.py +65 -0
- reader_workbench/domains/plate_reader/io/synergy_h1/__init__.py +6 -0
- reader_workbench/domains/plate_reader/io/synergy_h1/_kinetic.py +141 -0
- reader_workbench/domains/plate_reader/io/synergy_h1/_parser.py +295 -0
- reader_workbench/domains/plate_reader/io/synergy_h1/_shared.py +195 -0
- reader_workbench/domains/plate_reader/io/synergy_h1/_snapshot.py +130 -0
- reader_workbench/domains/plate_reader/ordering.py +59 -0
- reader_workbench/domains/plate_reader/plots/__init__.py +15 -0
- reader_workbench/domains/plate_reader/plots/_data.py +29 -0
- reader_workbench/domains/plate_reader/plots/common.py +346 -0
- reader_workbench/domains/plate_reader/plots/distributions.py +324 -0
- reader_workbench/domains/plate_reader/plots/dual_reporter_triptych.py +525 -0
- reader_workbench/domains/plate_reader/plots/dual_reporter_triptych_render.py +195 -0
- reader_workbench/domains/plate_reader/plots/four_state_event_window/__init__.py +26 -0
- reader_workbench/domains/plate_reader/plots/four_state_event_window/diagnostic.py +295 -0
- reader_workbench/domains/plate_reader/plots/four_state_event_window/diagnostic_components.py +149 -0
- reader_workbench/domains/plate_reader/plots/four_state_event_window/diagnostic_render.py +308 -0
- reader_workbench/domains/plate_reader/plots/four_state_event_window/diagnostic_style.py +51 -0
- reader_workbench/domains/plate_reader/plots/four_state_event_window/schema.py +8 -0
- reader_workbench/domains/plate_reader/plots/four_state_event_window/summary.py +140 -0
- reader_workbench/domains/plate_reader/plots/grouping.py +53 -0
- reader_workbench/domains/plate_reader/plots/panels/__init__.py +12 -0
- reader_workbench/domains/plate_reader/plots/panels/snapshot.py +161 -0
- reader_workbench/domains/plate_reader/plots/panels/snapshot_data.py +91 -0
- reader_workbench/domains/plate_reader/plots/panels/time_series.py +296 -0
- reader_workbench/domains/plate_reader/plots/single_reporter_diagnostic.py +435 -0
- reader_workbench/domains/plate_reader/plots/single_reporter_diagnostic_render.py +300 -0
- reader_workbench/domains/plate_reader/plots/snapshot_barplot/__init__.py +315 -0
- reader_workbench/domains/plate_reader/plots/snapshot_barplot/planning.py +168 -0
- reader_workbench/domains/plate_reader/plots/snapshot_heatmap/__init__.py +205 -0
- reader_workbench/domains/plate_reader/plots/snapshot_heatmap/inputs.py +103 -0
- reader_workbench/domains/plate_reader/plots/time_series.py +317 -0
- reader_workbench/domains/plate_reader/plots/ts_and_snap/__init__.py +479 -0
- reader_workbench/domains/plate_reader/plots/ts_and_snap/planning.py +283 -0
- reader_workbench/domains/time_series/__init__.py +29 -0
- reader_workbench/domains/time_series/aggregation.py +60 -0
- reader_workbench/domains/time_series/contracts.py +368 -0
- reader_workbench/domains/time_series/reduction.py +395 -0
- reader_workbench/errors.py +55 -0
- reader_workbench/maintenance/__init__.py +6 -0
- reader_workbench/maintenance/docs.py +335 -0
- reader_workbench/maintenance/model.py +28 -0
- reader_workbench/maintenance/release.py +39 -0
- reader_workbench/maintenance/skills.py +124 -0
- reader_workbench/plotting/__init__.py +20 -0
- reader_workbench/plotting/mpl.py +56 -0
- reader_workbench/plotting/sinks.py +69 -0
- reader_workbench/plotting/style.py +175 -0
- reader_workbench/plotting/utils.py +27 -0
- reader_workbench/plugins/__init__.py +1 -0
- reader_workbench/plugins/catalog.py +33 -0
- reader_workbench/plugins/export/__init__.py +0 -0
- reader_workbench/plugins/export/_paths.py +21 -0
- reader_workbench/plugins/export/csv.py +41 -0
- reader_workbench/plugins/export/xlsx.py +44 -0
- reader_workbench/plugins/ingest/__init__.py +0 -0
- reader_workbench/plugins/ingest/_discovery.py +58 -0
- reader_workbench/plugins/ingest/discovery_policy.py +66 -0
- reader_workbench/plugins/ingest/flow_cytometer.py +139 -0
- reader_workbench/plugins/ingest/synergy_h1.py +234 -0
- reader_workbench/plugins/manifests/__init__.py +1 -0
- reader_workbench/plugins/manifests/export.py +29 -0
- reader_workbench/plugins/manifests/ingest.py +29 -0
- reader_workbench/plugins/manifests/plot.py +161 -0
- reader_workbench/plugins/manifests/transform.py +172 -0
- reader_workbench/plugins/manifests/validator.py +18 -0
- reader_workbench/plugins/plot/__init__.py +0 -0
- reader_workbench/plugins/plot/_shared.py +55 -0
- reader_workbench/plugins/plot/cytometry_diagnostic.py +54 -0
- reader_workbench/plugins/plot/distributions.py +58 -0
- reader_workbench/plugins/plot/dual_reporter_triptych.py +224 -0
- reader_workbench/plugins/plot/four_state_event_window_diagnostic.py +133 -0
- reader_workbench/plugins/plot/four_state_event_window_summary.py +53 -0
- reader_workbench/plugins/plot/four_state_vector_collection.py +45 -0
- reader_workbench/plugins/plot/four_state_vector_diagnostic.py +105 -0
- reader_workbench/plugins/plot/four_state_vector_heatmap.py +63 -0
- reader_workbench/plugins/plot/logic_symmetry.py +56 -0
- reader_workbench/plugins/plot/single_reporter_diagnostic.py +279 -0
- reader_workbench/plugins/plot/snapshot_barplot.py +65 -0
- reader_workbench/plugins/plot/snapshot_heatmap.py +104 -0
- reader_workbench/plugins/plot/time_series.py +114 -0
- reader_workbench/plugins/plot/ts_and_snap.py +210 -0
- reader_workbench/plugins/transform/__init__.py +0 -0
- reader_workbench/plugins/transform/_four_state_vector.py +204 -0
- reader_workbench/plugins/transform/_labeling.py +109 -0
- reader_workbench/plugins/transform/alias.py +70 -0
- reader_workbench/plugins/transform/assay_labels.py +62 -0
- reader_workbench/plugins/transform/blank.py +79 -0
- reader_workbench/plugins/transform/crosstalk_pairs.py +180 -0
- reader_workbench/plugins/transform/cytometry_gating.py +120 -0
- reader_workbench/plugins/transform/fold_change.py +79 -0
- reader_workbench/plugins/transform/four_state_event_window.py +93 -0
- reader_workbench/plugins/transform/four_state_vector.py +62 -0
- reader_workbench/plugins/transform/four_state_vector_collection.py +41 -0
- reader_workbench/plugins/transform/logic_symmetry.py +67 -0
- reader_workbench/plugins/transform/outlier_filter.py +60 -0
- reader_workbench/plugins/transform/overflow.py +197 -0
- reader_workbench/plugins/transform/ratio.py +237 -0
- reader_workbench/plugins/transform/sample_map.py +170 -0
- reader_workbench/plugins/transform/sample_metadata.py +94 -0
- reader_workbench/plugins/validator/__init__.py +1 -0
- reader_workbench/plugins/validator/to_tidy_plus_map.py +155 -0
- reader_workbench/protocols/__init__.py +80 -0
- reader_workbench/protocols/_builtins_plate_reader_growth.py +179 -0
- reader_workbench/protocols/_builtins_plate_reader_variants.py +274 -0
- reader_workbench/protocols/builtins.py +1656 -0
- reader_workbench/protocols/compiler.py +22 -0
- reader_workbench/protocols/compilers/__init__.py +1 -0
- reader_workbench/protocols/compilers/common.py +100 -0
- reader_workbench/protocols/compilers/cytometry.py +87 -0
- reader_workbench/protocols/compilers/generic.py +14 -0
- reader_workbench/protocols/compilers/logic.py +245 -0
- reader_workbench/protocols/compilers/plate_reader.py +937 -0
- reader_workbench/protocols/compilers/plate_reader_pipeline.py +197 -0
- reader_workbench/protocols/model.py +1486 -0
- reader_workbench/protocols/semantic_coverage.py +234 -0
- reader_workbench/runtime/__init__.py +12 -0
- reader_workbench/runtime/builtin.py +23 -0
- reader_workbench/runtime/model.py +42 -0
- reader_workbench/workbench/__init__.py +60 -0
- reader_workbench/workbench/assets/__init__.py +22 -0
- reader_workbench/workbench/assets/types.py +118 -0
- reader_workbench/workbench/audit/__init__.py +5 -0
- reader_workbench/workbench/audit/experiments.py +307 -0
- reader_workbench/workbench/audit/staging.py +187 -0
- reader_workbench/workbench/cli/__init__.py +51 -0
- reader_workbench/workbench/cli/_lazy.py +9 -0
- reader_workbench/workbench/cli/_records_view.py +150 -0
- reader_workbench/workbench/cli/_surface_execution.py +443 -0
- reader_workbench/workbench/cli/audit.py +95 -0
- reader_workbench/workbench/cli/automation.py +229 -0
- reader_workbench/workbench/cli/demo.py +46 -0
- reader_workbench/workbench/cli/dop.py +91 -0
- reader_workbench/workbench/cli/experiments.py +635 -0
- reader_workbench/workbench/cli/helpers.py +232 -0
- reader_workbench/workbench/cli/main.py +59 -0
- reader_workbench/workbench/cli/maintenance.py +82 -0
- reader_workbench/workbench/cli/notebooks.py +260 -0
- reader_workbench/workbench/cli/pagination.py +117 -0
- reader_workbench/workbench/cli/protocols.py +336 -0
- reader_workbench/workbench/cli/shared.py +309 -0
- reader_workbench/workbench/cli/surfaces.py +534 -0
- reader_workbench/workbench/cli/verification.py +128 -0
- reader_workbench/workbench/commands.py +10 -0
- reader_workbench/workbench/config/__init__.py +47 -0
- reader_workbench/workbench/config/identity.py +13 -0
- reader_workbench/workbench/config/load.py +405 -0
- reader_workbench/workbench/config/model.py +274 -0
- reader_workbench/workbench/context.py +26 -0
- reader_workbench/workbench/decl/__init__.py +31 -0
- reader_workbench/workbench/decl/build.py +190 -0
- reader_workbench/workbench/decl/model.py +81 -0
- reader_workbench/workbench/dop/__init__.py +12 -0
- reader_workbench/workbench/dop/builtins.py +261 -0
- reader_workbench/workbench/dop/model.py +209 -0
- reader_workbench/workbench/engine/__init__.py +42 -0
- reader_workbench/workbench/engine/_shared.py +76 -0
- reader_workbench/workbench/engine/contracts.py +283 -0
- reader_workbench/workbench/engine/execution.py +326 -0
- reader_workbench/workbench/engine/file_outputs.py +260 -0
- reader_workbench/workbench/engine/inputs.py +161 -0
- reader_workbench/workbench/engine/invocations.py +507 -0
- reader_workbench/workbench/engine/planning.py +72 -0
- reader_workbench/workbench/engine/runtime.py +464 -0
- reader_workbench/workbench/engine/setup.py +149 -0
- reader_workbench/workbench/engine/validation.py +684 -0
- reader_workbench/workbench/experiment/__init__.py +47 -0
- reader_workbench/workbench/experiment/model.py +381 -0
- reader_workbench/workbench/experiments.py +133 -0
- reader_workbench/workbench/graph/__init__.py +47 -0
- reader_workbench/workbench/graph/nodes.py +102 -0
- reader_workbench/workbench/graph/normalize.py +177 -0
- reader_workbench/workbench/graph/refs.py +148 -0
- reader_workbench/workbench/input_discovery.py +19 -0
- reader_workbench/workbench/inspection/__init__.py +3 -0
- reader_workbench/workbench/inspection/catalogs.py +128 -0
- reader_workbench/workbench/inspection/common.py +92 -0
- reader_workbench/workbench/inspection/dop.py +64 -0
- reader_workbench/workbench/inspection/experiments.py +449 -0
- reader_workbench/workbench/inspection/inventory.py +68 -0
- reader_workbench/workbench/inspection/protocols.py +368 -0
- reader_workbench/workbench/inspection/readiness.py +333 -0
- reader_workbench/workbench/inspection/reports.py +367 -0
- reader_workbench/workbench/inspection/results.py +166 -0
- reader_workbench/workbench/inspection/runtime.py +287 -0
- reader_workbench/workbench/inspection/semantics.py +192 -0
- reader_workbench/workbench/inspection/validation.py +30 -0
- reader_workbench/workbench/notebooks/__init__.py +17 -0
- reader_workbench/workbench/notebooks/_launch_registry.py +112 -0
- reader_workbench/workbench/notebooks/_launch_runtime.py +104 -0
- reader_workbench/workbench/notebooks/components/__init__.py +21 -0
- reader_workbench/workbench/notebooks/components/deliverables.py +403 -0
- reader_workbench/workbench/notebooks/components/overview.py +119 -0
- reader_workbench/workbench/notebooks/eda.marimo.py.txt +153 -0
- reader_workbench/workbench/notebooks/launch.py +274 -0
- reader_workbench/workbench/notebooks/presentation.py +136 -0
- reader_workbench/workbench/notebooks/scaffold.py +60 -0
- reader_workbench/workbench/ontology.py +78 -0
- reader_workbench/workbench/paths.py +44 -0
- reader_workbench/workbench/ports/__init__.py +31 -0
- reader_workbench/workbench/ports/model.py +168 -0
- reader_workbench/workbench/records/__init__.py +44 -0
- reader_workbench/workbench/records/epoch.py +329 -0
- reader_workbench/workbench/records/evidence.py +247 -0
- reader_workbench/workbench/records/identity.py +87 -0
- reader_workbench/workbench/records/locking.py +185 -0
- reader_workbench/workbench/records/model.py +711 -0
- reader_workbench/workbench/records/sources.py +73 -0
- reader_workbench/workbench/records/store.py +1022 -0
- reader_workbench/workbench/records/verification.py +998 -0
- reader_workbench/workbench/registry.py +333 -0
- reader_workbench/workbench/spec_overrides.py +215 -0
- reader_workbench-1.0.0.dist-info/METADATA +91 -0
- reader_workbench-1.0.0.dist-info/RECORD +293 -0
- reader_workbench-1.0.0.dist-info/WHEEL +5 -0
- reader_workbench-1.0.0.dist-info/entry_points.txt +2 -0
- reader_workbench-1.0.0.dist-info/licenses/LICENSE +21 -0
- reader_workbench-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
from rich import box
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
from reader_workbench.errors import ConfigError
|
|
11
|
+
from reader_workbench.protocols import ProtocolBinding
|
|
12
|
+
from reader_workbench.protocols.model import ProtocolAnalysisChoiceRef, ProtocolBindingValueRef
|
|
13
|
+
|
|
14
|
+
from .runtime import compiled_workbench_payload, record_producer_map
|
|
15
|
+
from .semantics import semantic_program_payload
|
|
16
|
+
|
|
17
|
+
PROTOCOL_DESCRIPTOR_SECTIONS = ("identity", "authoring", "semantics", "defaults", "compiled")
|
|
18
|
+
_PROTOCOL_IDENTITY_FIELDS = ("protocol", "domain", "family", "summary", "tags")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _table(title: str) -> Table:
|
|
22
|
+
return Table(
|
|
23
|
+
title=f"[title]{title}[/title]",
|
|
24
|
+
title_justify="left",
|
|
25
|
+
header_style="bold",
|
|
26
|
+
box=box.ROUNDED,
|
|
27
|
+
expand=True,
|
|
28
|
+
show_lines=False,
|
|
29
|
+
show_edge=True,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def protocol_surface_rows(fields) -> list[tuple[str, str, str, str, str]]:
|
|
34
|
+
rows: list[tuple[str, str, str, str, str]] = []
|
|
35
|
+
for field in fields:
|
|
36
|
+
rows.extend(field.iter_rows())
|
|
37
|
+
return rows
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _protocol_field_payload(field, *, prefix: str = "") -> dict[str, object]:
|
|
41
|
+
path = f"{prefix}{field.key}"
|
|
42
|
+
payload: dict[str, object] = {
|
|
43
|
+
"key": field.key,
|
|
44
|
+
"path": path,
|
|
45
|
+
"kind": field.kind,
|
|
46
|
+
"summary": field.summary,
|
|
47
|
+
"required": field.required,
|
|
48
|
+
"allow_none": field.allow_none,
|
|
49
|
+
"allow_unknown": field.allow_unknown,
|
|
50
|
+
}
|
|
51
|
+
if field.has_default:
|
|
52
|
+
payload["default"] = deepcopy(field.default)
|
|
53
|
+
if field.has_example:
|
|
54
|
+
payload["example"] = deepcopy(field.example)
|
|
55
|
+
if field.choices:
|
|
56
|
+
payload["choices"] = list(field.choices)
|
|
57
|
+
if field.children:
|
|
58
|
+
payload["children"] = [_protocol_field_payload(child, prefix=f"{path}.") for child in field.children]
|
|
59
|
+
return payload
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def protocol_surface_payload(fields) -> list[dict[str, object]]:
|
|
63
|
+
return [_protocol_field_payload(field) for field in fields]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def protocol_surface_table(title: str, rows: list[tuple[str, str, str, str, str]]) -> Table:
|
|
67
|
+
table = _table(title)
|
|
68
|
+
table.add_column("Path", style="accent")
|
|
69
|
+
table.add_column("Type")
|
|
70
|
+
table.add_column("Required", justify="center")
|
|
71
|
+
table.add_column("Default")
|
|
72
|
+
table.add_column("Summary")
|
|
73
|
+
for path, kind, required, default, summary in rows:
|
|
74
|
+
table.add_row(path, kind, required, default, summary)
|
|
75
|
+
return table
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def protocol_plot_profiles_table(descriptor) -> Table:
|
|
79
|
+
table = _table("Plot Profiles")
|
|
80
|
+
table.add_column("id", style="accent")
|
|
81
|
+
table.add_column("figures")
|
|
82
|
+
table.add_column("summary")
|
|
83
|
+
for item in descriptor.plot_profiles:
|
|
84
|
+
table.add_row(item.id, ", ".join(item.figures), item.summary)
|
|
85
|
+
return table
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def protocol_plot_outputs_table(descriptor) -> Table:
|
|
89
|
+
table = _table("Plot Outputs")
|
|
90
|
+
table.add_column("id", style="accent")
|
|
91
|
+
table.add_column("kind")
|
|
92
|
+
table.add_column("primary", justify="center")
|
|
93
|
+
table.add_column("summary")
|
|
94
|
+
for item in descriptor.figures:
|
|
95
|
+
table.add_row(item.id, item.kind, "yes" if item.primary else "no", item.summary)
|
|
96
|
+
return table
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def protocol_artifacts_table(descriptor) -> Table:
|
|
100
|
+
table = _table("Export Artifacts")
|
|
101
|
+
table.add_column("id", style="accent")
|
|
102
|
+
table.add_column("summary")
|
|
103
|
+
for item in descriptor.artifacts:
|
|
104
|
+
table.add_row(item.id, item.summary)
|
|
105
|
+
return table
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def protocol_pipeline_table(steps) -> Table:
|
|
109
|
+
table = _table("Default Pipeline")
|
|
110
|
+
table.add_column("#", justify="right", style="muted")
|
|
111
|
+
table.add_column("stage", style="accent")
|
|
112
|
+
table.add_column("id", overflow="fold")
|
|
113
|
+
table.add_column("plugin", overflow="fold")
|
|
114
|
+
for idx, step in enumerate(steps, 1):
|
|
115
|
+
stage = step.plugin.split("/", 1)[0]
|
|
116
|
+
table.add_row(str(idx), stage, step.id, step.plugin)
|
|
117
|
+
return table
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def protocol_surface_impl_table(title: str, steps, summaries: dict[str, str], *, binding_display) -> Table:
|
|
121
|
+
table = _table(title)
|
|
122
|
+
table.add_column("id", style="accent", overflow="fold")
|
|
123
|
+
table.add_column("plugin", overflow="fold")
|
|
124
|
+
table.add_column("from", overflow="fold")
|
|
125
|
+
table.add_column("summary", overflow="fold")
|
|
126
|
+
for step in steps:
|
|
127
|
+
from_refs = ", ".join(f"{label} <- {binding_display(ref)}" for label, ref in (step.reads or {}).items()) or "—"
|
|
128
|
+
table.add_row(step.id, step.plugin, from_refs, summaries.get(step.id, "—"))
|
|
129
|
+
return table
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
_EXAMPLE_OMIT = object()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _protocol_field_example_value(field) -> object:
|
|
136
|
+
if field.has_example:
|
|
137
|
+
return deepcopy(field.example)
|
|
138
|
+
if field.kind == "mapping":
|
|
139
|
+
example: dict[str, object] = {}
|
|
140
|
+
for child in field.children:
|
|
141
|
+
child_value = _protocol_field_example_value(child)
|
|
142
|
+
if child_value is _EXAMPLE_OMIT:
|
|
143
|
+
continue
|
|
144
|
+
example[child.key] = child_value
|
|
145
|
+
if example:
|
|
146
|
+
return example
|
|
147
|
+
if field.has_default:
|
|
148
|
+
return deepcopy(field.default)
|
|
149
|
+
if field.required:
|
|
150
|
+
return {}
|
|
151
|
+
return _EXAMPLE_OMIT
|
|
152
|
+
if field.has_default:
|
|
153
|
+
return deepcopy(field.default)
|
|
154
|
+
if field.required:
|
|
155
|
+
return "<required>"
|
|
156
|
+
return _EXAMPLE_OMIT
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _protocol_surface_example(fields) -> dict[str, object]:
|
|
160
|
+
example: dict[str, object] = {}
|
|
161
|
+
for field in fields:
|
|
162
|
+
value = _protocol_field_example_value(field)
|
|
163
|
+
if value is _EXAMPLE_OMIT:
|
|
164
|
+
continue
|
|
165
|
+
example[field.key] = value
|
|
166
|
+
return example
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def protocol_authoring_output_payload(descriptor) -> dict[str, object]:
|
|
170
|
+
return {
|
|
171
|
+
"default_plot_profile": descriptor.default_plot_profile,
|
|
172
|
+
"plot_profiles": [
|
|
173
|
+
{
|
|
174
|
+
"id": item.id,
|
|
175
|
+
"figures": list(item.figures),
|
|
176
|
+
"summary": item.summary,
|
|
177
|
+
}
|
|
178
|
+
for item in descriptor.plot_profiles
|
|
179
|
+
],
|
|
180
|
+
"figures": [
|
|
181
|
+
{
|
|
182
|
+
"id": item.id,
|
|
183
|
+
"kind": item.kind,
|
|
184
|
+
"primary": item.primary,
|
|
185
|
+
"summary": item.summary,
|
|
186
|
+
}
|
|
187
|
+
for item in descriptor.figures
|
|
188
|
+
],
|
|
189
|
+
"artifacts": [
|
|
190
|
+
{
|
|
191
|
+
"id": item.id,
|
|
192
|
+
"summary": item.summary,
|
|
193
|
+
"default": item.default,
|
|
194
|
+
}
|
|
195
|
+
for item in descriptor.artifacts
|
|
196
|
+
],
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def protocol_example_document(descriptor) -> dict[str, object]:
|
|
201
|
+
protocol_block: dict[str, object] = {"id": descriptor.protocol}
|
|
202
|
+
inputs = _protocol_surface_example(descriptor.input_fields)
|
|
203
|
+
if inputs:
|
|
204
|
+
protocol_block["inputs"] = inputs
|
|
205
|
+
analysis = _protocol_surface_example(descriptor.analysis_fields)
|
|
206
|
+
if analysis:
|
|
207
|
+
protocol_block["analysis"] = analysis
|
|
208
|
+
|
|
209
|
+
outputs: dict[str, object] = {}
|
|
210
|
+
if descriptor.plot_profiles or descriptor.figures:
|
|
211
|
+
plots: dict[str, object] = {}
|
|
212
|
+
if descriptor.default_plot_profile is not None:
|
|
213
|
+
plots["profile"] = descriptor.default_plot_profile
|
|
214
|
+
elif descriptor.figures:
|
|
215
|
+
plots["include"] = [item.id for item in descriptor.figures if item.primary]
|
|
216
|
+
if plots:
|
|
217
|
+
outputs["plots"] = plots
|
|
218
|
+
default_artifacts = [item.id for item in descriptor.artifacts if item.default]
|
|
219
|
+
if default_artifacts:
|
|
220
|
+
outputs["exports"] = {"include": default_artifacts}
|
|
221
|
+
protocol_block["outputs"] = outputs
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
"schema": "reader/v8",
|
|
225
|
+
"experiment": {"id": "example_experiment"},
|
|
226
|
+
"protocol": protocol_block,
|
|
227
|
+
"resources": {item.id: {"kind": item.kind, "path": item.path} for item in descriptor.resources},
|
|
228
|
+
"annotations": deepcopy(descriptor.example_annotations),
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def protocol_example_config(descriptor) -> str:
|
|
233
|
+
return yaml.safe_dump(protocol_example_document(descriptor), sort_keys=False)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _runtime_default_value_payload(value: Any) -> Any:
|
|
237
|
+
if isinstance(value, ProtocolBindingValueRef):
|
|
238
|
+
payload: dict[str, object] = {
|
|
239
|
+
"source": f"protocol.inputs.{value.key}",
|
|
240
|
+
}
|
|
241
|
+
if value.has_default:
|
|
242
|
+
payload["default"] = _runtime_default_value_payload(value.default)
|
|
243
|
+
else:
|
|
244
|
+
payload["required"] = True
|
|
245
|
+
return payload
|
|
246
|
+
if isinstance(value, ProtocolAnalysisChoiceRef):
|
|
247
|
+
payload: dict[str, object] = {
|
|
248
|
+
"source": f"protocol.analysis.{value.key}",
|
|
249
|
+
"cases": {key: _runtime_default_value_payload(item) for key, item in value.cases.items()},
|
|
250
|
+
}
|
|
251
|
+
if value.has_default:
|
|
252
|
+
payload["default"] = _runtime_default_value_payload(value.default)
|
|
253
|
+
else:
|
|
254
|
+
payload["required"] = True
|
|
255
|
+
return payload
|
|
256
|
+
if isinstance(value, dict):
|
|
257
|
+
return {key: _runtime_default_value_payload(item) for key, item in value.items()}
|
|
258
|
+
if isinstance(value, list):
|
|
259
|
+
return [_runtime_default_value_payload(item) for item in value]
|
|
260
|
+
if isinstance(value, tuple):
|
|
261
|
+
return [_runtime_default_value_payload(item) for item in value]
|
|
262
|
+
return deepcopy(value)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def protocol_runtime_defaults_payload(plugin_defaults) -> list[dict[str, object]]:
|
|
266
|
+
return [
|
|
267
|
+
{
|
|
268
|
+
"plugin": item.plugin,
|
|
269
|
+
"summary": item.summary,
|
|
270
|
+
"parameters": _runtime_default_value_payload(item.with_),
|
|
271
|
+
}
|
|
272
|
+
for item in plugin_defaults
|
|
273
|
+
]
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def protocol_descriptor_payload(descriptor, *, runtime) -> dict[str, object]:
|
|
277
|
+
try:
|
|
278
|
+
bound_protocol = runtime.bind_protocol(ProtocolBinding(id=descriptor.protocol))
|
|
279
|
+
except ConfigError as exc:
|
|
280
|
+
semantic_program = descriptor.semantic_program()
|
|
281
|
+
compiled_payload = {
|
|
282
|
+
"status": "requires_authoring",
|
|
283
|
+
"reason": str(exc),
|
|
284
|
+
"pipeline": [],
|
|
285
|
+
"plots": [],
|
|
286
|
+
"exports": [],
|
|
287
|
+
"semantic_program": semantic_program_payload(semantic_program),
|
|
288
|
+
}
|
|
289
|
+
else:
|
|
290
|
+
compiled_plan = bound_protocol.compile()
|
|
291
|
+
semantic_program = compiled_plan.semantic_program
|
|
292
|
+
record_producers = record_producer_map(compiled_plan.pipeline, runtime=runtime)
|
|
293
|
+
compiled_payload = compiled_workbench_payload(
|
|
294
|
+
bound_protocol=bound_protocol,
|
|
295
|
+
pipeline_steps=compiled_plan.pipeline,
|
|
296
|
+
plot_steps=compiled_plan.plots,
|
|
297
|
+
export_steps=compiled_plan.exports,
|
|
298
|
+
runtime=runtime,
|
|
299
|
+
record_producers=record_producers,
|
|
300
|
+
)
|
|
301
|
+
compiled_payload["semantic_program"] = semantic_program_payload(semantic_program)
|
|
302
|
+
return {
|
|
303
|
+
"protocol": descriptor.protocol,
|
|
304
|
+
"domain": descriptor.domain,
|
|
305
|
+
"family": descriptor.family,
|
|
306
|
+
"summary": descriptor.summary,
|
|
307
|
+
"tags": list(descriptor.tags),
|
|
308
|
+
"authoring": {
|
|
309
|
+
"inputs": protocol_surface_payload(descriptor.input_fields),
|
|
310
|
+
"analysis": protocol_surface_payload(descriptor.analysis_fields),
|
|
311
|
+
"resources": [
|
|
312
|
+
{
|
|
313
|
+
"id": item.id,
|
|
314
|
+
"kind": item.kind,
|
|
315
|
+
"path": item.path,
|
|
316
|
+
"summary": item.summary,
|
|
317
|
+
}
|
|
318
|
+
for item in descriptor.resources
|
|
319
|
+
],
|
|
320
|
+
"outputs": protocol_authoring_output_payload(descriptor),
|
|
321
|
+
"starter_config": protocol_example_document(descriptor),
|
|
322
|
+
},
|
|
323
|
+
"semantics": {
|
|
324
|
+
"factors": [
|
|
325
|
+
{
|
|
326
|
+
"name": item.name,
|
|
327
|
+
"role": item.role,
|
|
328
|
+
"summary": item.summary,
|
|
329
|
+
"required": item.required,
|
|
330
|
+
"repeatable": item.repeatable,
|
|
331
|
+
}
|
|
332
|
+
for item in descriptor.factors
|
|
333
|
+
],
|
|
334
|
+
"effect_signs": [
|
|
335
|
+
{
|
|
336
|
+
"target": item.target,
|
|
337
|
+
"expected_sign": item.expected_sign,
|
|
338
|
+
"summary": item.summary,
|
|
339
|
+
}
|
|
340
|
+
for item in descriptor.effect_signs
|
|
341
|
+
],
|
|
342
|
+
"program": semantic_program_payload(semantic_program, include_execution=False),
|
|
343
|
+
},
|
|
344
|
+
"implementation": {
|
|
345
|
+
"defaults": protocol_runtime_defaults_payload(descriptor.execution.plugin_defaults),
|
|
346
|
+
"compiled": compiled_payload,
|
|
347
|
+
},
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def protocol_descriptor_section_payload(
|
|
352
|
+
payload: dict[str, object],
|
|
353
|
+
*,
|
|
354
|
+
section: str,
|
|
355
|
+
) -> dict[str, object]:
|
|
356
|
+
"""Project a protocol descriptor through a stable semantic section name."""
|
|
357
|
+
|
|
358
|
+
if section not in PROTOCOL_DESCRIPTOR_SECTIONS:
|
|
359
|
+
raise ValueError(f"Unknown protocol descriptor section {section!r}")
|
|
360
|
+
projected = {field: deepcopy(payload[field]) for field in _PROTOCOL_IDENTITY_FIELDS}
|
|
361
|
+
if section in {"authoring", "semantics"}:
|
|
362
|
+
projected[section] = deepcopy(payload[section])
|
|
363
|
+
elif section != "identity":
|
|
364
|
+
implementation = payload["implementation"]
|
|
365
|
+
if not isinstance(implementation, dict):
|
|
366
|
+
raise ValueError("Protocol descriptor payload does not contain implementation details")
|
|
367
|
+
projected[section] = deepcopy(implementation[section])
|
|
368
|
+
return projected
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from reader_workbench.errors import RecordError
|
|
7
|
+
from reader_workbench.runtime import ReaderRuntime
|
|
8
|
+
from reader_workbench.workbench.commands import reader_command
|
|
9
|
+
from reader_workbench.workbench.decl import WorkbenchDecl
|
|
10
|
+
from reader_workbench.workbench.engine.planning import build_next_steps
|
|
11
|
+
from reader_workbench.workbench.engine.validation import validation_summary
|
|
12
|
+
from reader_workbench.workbench.graph import RecordRef, resolve_workbench
|
|
13
|
+
from reader_workbench.workbench.records import verify_record_store
|
|
14
|
+
|
|
15
|
+
from .common import summarize_outputs_dir
|
|
16
|
+
from .runtime import workbench_record_verification_scope
|
|
17
|
+
|
|
18
|
+
READINESS_STATES = frozenset(
|
|
19
|
+
{
|
|
20
|
+
"config_error",
|
|
21
|
+
"draft",
|
|
22
|
+
"template",
|
|
23
|
+
"dependency_blocked",
|
|
24
|
+
"blocked",
|
|
25
|
+
"runnable",
|
|
26
|
+
"uncataloged_outputs_present",
|
|
27
|
+
"catalog_ready",
|
|
28
|
+
"records_ready",
|
|
29
|
+
}
|
|
30
|
+
)
|
|
31
|
+
READINESS_CAPABILITY_KEYS = frozenset(
|
|
32
|
+
{
|
|
33
|
+
"run",
|
|
34
|
+
"records",
|
|
35
|
+
"verify",
|
|
36
|
+
"plot",
|
|
37
|
+
"export",
|
|
38
|
+
"notebook_scaffold",
|
|
39
|
+
"notebook_read_records",
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def config_error_readiness_payload(error: str) -> dict[str, object]:
|
|
45
|
+
return {
|
|
46
|
+
"state": "config_error",
|
|
47
|
+
"summary": "config error",
|
|
48
|
+
"preflight": {
|
|
49
|
+
"status": "error",
|
|
50
|
+
"issues": 1,
|
|
51
|
+
"files": "not checked",
|
|
52
|
+
"dependencies": "not checked",
|
|
53
|
+
},
|
|
54
|
+
"records": {
|
|
55
|
+
"catalog": False,
|
|
56
|
+
"available": False,
|
|
57
|
+
"verification": None,
|
|
58
|
+
"uncataloged_outputs_present": False,
|
|
59
|
+
},
|
|
60
|
+
"capabilities": {
|
|
61
|
+
"run": False,
|
|
62
|
+
"records": False,
|
|
63
|
+
"verify": False,
|
|
64
|
+
"plot": False,
|
|
65
|
+
"export": False,
|
|
66
|
+
"notebook_scaffold": False,
|
|
67
|
+
"notebook_read_records": False,
|
|
68
|
+
},
|
|
69
|
+
"errors": [error],
|
|
70
|
+
"next_steps": [],
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _non_active_lifecycle_payload(*, lifecycle: str, job_path: Path) -> dict[str, object]:
|
|
75
|
+
next_step = {
|
|
76
|
+
"command": reader_command("validate", job_path, "--no-files", "--format", "json"),
|
|
77
|
+
"description": "Check the config shape without treating this non-active experiment as ready to run.",
|
|
78
|
+
}
|
|
79
|
+
summary_by_lifecycle = {
|
|
80
|
+
"draft": "draft experiment; add inputs and switch lifecycle to active when ready",
|
|
81
|
+
"template": "template scaffold; copy/customize before treating it as a runnable experiment",
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
"state": lifecycle,
|
|
85
|
+
"summary": summary_by_lifecycle.get(lifecycle, lifecycle),
|
|
86
|
+
"preflight": {
|
|
87
|
+
"status": "skipped",
|
|
88
|
+
"issues": 0,
|
|
89
|
+
"files": "not evaluated for non-active lifecycle",
|
|
90
|
+
"dependencies": "not evaluated for non-active lifecycle",
|
|
91
|
+
},
|
|
92
|
+
"records": {
|
|
93
|
+
"catalog": False,
|
|
94
|
+
"available": False,
|
|
95
|
+
"verification": None,
|
|
96
|
+
"uncataloged_outputs_present": False,
|
|
97
|
+
},
|
|
98
|
+
"capabilities": {
|
|
99
|
+
"run": False,
|
|
100
|
+
"records": False,
|
|
101
|
+
"verify": False,
|
|
102
|
+
"plot": False,
|
|
103
|
+
"export": False,
|
|
104
|
+
"notebook_scaffold": True,
|
|
105
|
+
"notebook_read_records": False,
|
|
106
|
+
},
|
|
107
|
+
"errors": [],
|
|
108
|
+
"next_steps": [next_step],
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def readiness_summary_text(payload: dict[str, object] | None) -> str:
|
|
113
|
+
if not isinstance(payload, dict):
|
|
114
|
+
return "—"
|
|
115
|
+
summary = str(payload.get("summary") or "").strip()
|
|
116
|
+
return summary or str(payload.get("state") or "—")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def readiness_primary_command(payload: dict[str, object] | None) -> str:
|
|
120
|
+
if not isinstance(payload, dict):
|
|
121
|
+
return "—"
|
|
122
|
+
next_steps = payload.get("next_steps") or []
|
|
123
|
+
if not isinstance(next_steps, list) or not next_steps:
|
|
124
|
+
return "—"
|
|
125
|
+
first = next_steps[0]
|
|
126
|
+
if not isinstance(first, dict):
|
|
127
|
+
return "—"
|
|
128
|
+
command = str(first.get("command") or "").strip()
|
|
129
|
+
return command or "—"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _surface_record_dependencies_verified(steps, verification_report: dict[str, object] | None) -> bool:
|
|
133
|
+
required_record_ids = {
|
|
134
|
+
ref.record_id for step in steps for ref in (step.reads or {}).values() if isinstance(ref, RecordRef)
|
|
135
|
+
}
|
|
136
|
+
if not verification_report or verification_report.get("status") != "ok":
|
|
137
|
+
return False
|
|
138
|
+
if not required_record_ids:
|
|
139
|
+
return True
|
|
140
|
+
record_statuses = {
|
|
141
|
+
str(record.get("record_id")): record.get("status")
|
|
142
|
+
for record in verification_report.get("records", [])
|
|
143
|
+
if isinstance(record, dict)
|
|
144
|
+
}
|
|
145
|
+
return all(record_statuses.get(record_id) == "ok" for record_id in required_record_ids)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def experiment_readiness_payload(
|
|
149
|
+
*,
|
|
150
|
+
job_path: Path,
|
|
151
|
+
decl: WorkbenchDecl,
|
|
152
|
+
runtime: ReaderRuntime,
|
|
153
|
+
check_files: bool = True,
|
|
154
|
+
) -> dict[str, object]:
|
|
155
|
+
if decl.experiment.lifecycle != "active":
|
|
156
|
+
return _non_active_lifecycle_payload(lifecycle=decl.experiment.lifecycle, job_path=job_path)
|
|
157
|
+
|
|
158
|
+
summary = validation_summary(
|
|
159
|
+
decl,
|
|
160
|
+
check_files=check_files,
|
|
161
|
+
exp_root=decl.experiment.root,
|
|
162
|
+
runtime=runtime,
|
|
163
|
+
)
|
|
164
|
+
layout = decl.experiment_semantics.layout
|
|
165
|
+
workbench = resolve_workbench(decl)
|
|
166
|
+
outputs_dir = layout.outputs_dir
|
|
167
|
+
store = runtime.record_store(
|
|
168
|
+
outputs_dir,
|
|
169
|
+
plots_subdir=layout.plots_subdir,
|
|
170
|
+
exports_subdir=layout.exports_subdir,
|
|
171
|
+
experiment_root=decl.experiment.root,
|
|
172
|
+
create=False,
|
|
173
|
+
)
|
|
174
|
+
records_catalog = store.catalog_exists()
|
|
175
|
+
records_available = False
|
|
176
|
+
record_catalog_error: str | None = None
|
|
177
|
+
verification_report: dict[str, object] | None = None
|
|
178
|
+
verification_scope = workbench_record_verification_scope(workbench, runtime=runtime)
|
|
179
|
+
if records_catalog:
|
|
180
|
+
try:
|
|
181
|
+
current_records = tuple(
|
|
182
|
+
record for record in store.iter_latest_records() if verification_scope.includes(record)
|
|
183
|
+
)
|
|
184
|
+
records_available = bool(current_records)
|
|
185
|
+
if records_available:
|
|
186
|
+
verification_report = verify_record_store(
|
|
187
|
+
store,
|
|
188
|
+
experiment_root=decl.experiment.root,
|
|
189
|
+
expected_config_digest=decl.config_digest,
|
|
190
|
+
scope=verification_scope,
|
|
191
|
+
)
|
|
192
|
+
except RecordError as exc:
|
|
193
|
+
record_catalog_error = str(exc)
|
|
194
|
+
generated = summarize_outputs_dir(
|
|
195
|
+
outputs_dir,
|
|
196
|
+
plots_subdir=layout.plots_subdir,
|
|
197
|
+
exports_subdir=layout.exports_subdir,
|
|
198
|
+
notebooks_subdir=layout.notebooks_subdir,
|
|
199
|
+
)
|
|
200
|
+
uncataloged_outputs_present = (
|
|
201
|
+
any(generated[key] for key in ("records", "plots", "exports")) and not records_available
|
|
202
|
+
)
|
|
203
|
+
verified_records = bool(verification_report and verification_report["status"] == "ok")
|
|
204
|
+
plot_ready = verified_records and _surface_record_dependencies_verified(workbench.plots, verification_report)
|
|
205
|
+
export_ready = verified_records and _surface_record_dependencies_verified(workbench.exports, verification_report)
|
|
206
|
+
surfaces_waiting = (bool(workbench.plots) and not plot_ready) or (bool(workbench.exports) and not export_ready)
|
|
207
|
+
can_run = summary["status"] == "ok"
|
|
208
|
+
file_issues = int(summary["files"].get("issues") or 0)
|
|
209
|
+
dependency_issues = int(summary["dependencies"].get("issues") or 0)
|
|
210
|
+
if not can_run and dependency_issues and not file_issues:
|
|
211
|
+
state = "dependency_blocked"
|
|
212
|
+
summary_text = f"dependency blocked ({dependency_issues} issue(s))"
|
|
213
|
+
next_steps = [
|
|
214
|
+
{
|
|
215
|
+
"command": reader_command("validate", job_path, "--format", "json"),
|
|
216
|
+
"description": "Check missing runtime dependencies before running this assay.",
|
|
217
|
+
}
|
|
218
|
+
]
|
|
219
|
+
elif not can_run:
|
|
220
|
+
state = "blocked"
|
|
221
|
+
summary_text = f"blocked ({len(summary['errors'])} issue(s))"
|
|
222
|
+
next_steps = [
|
|
223
|
+
{
|
|
224
|
+
"command": reader_command("validate", job_path, "--format", "json"),
|
|
225
|
+
"description": "Check blocking file or dependency issues.",
|
|
226
|
+
}
|
|
227
|
+
]
|
|
228
|
+
elif record_catalog_error is not None:
|
|
229
|
+
state = "blocked"
|
|
230
|
+
summary_text = "blocked (invalid records catalog)"
|
|
231
|
+
next_steps = [
|
|
232
|
+
{
|
|
233
|
+
"command": reader_command("run", job_path, "--reset-records"),
|
|
234
|
+
"description": "Replace the invalid catalog and perform one complete pipeline rerun.",
|
|
235
|
+
}
|
|
236
|
+
]
|
|
237
|
+
elif records_available and verification_report and verification_report["status"] == "failed":
|
|
238
|
+
state = "blocked"
|
|
239
|
+
summary_text = "blocked (record verification failed)"
|
|
240
|
+
next_steps = [
|
|
241
|
+
{
|
|
242
|
+
"command": reader_command("verify", job_path, "--format", "json"),
|
|
243
|
+
"description": "Inspect source, dependency, config, or artifact drift before using persisted outputs.",
|
|
244
|
+
}
|
|
245
|
+
]
|
|
246
|
+
elif records_available and verification_report and verification_report["status"] == "unverifiable":
|
|
247
|
+
state = "catalog_ready"
|
|
248
|
+
summary_text = "catalog ready; provenance is not fully verifiable"
|
|
249
|
+
next_steps = [
|
|
250
|
+
{
|
|
251
|
+
"command": reader_command("verify", job_path, "--format", "json"),
|
|
252
|
+
"description": "Inspect missing or stale evidence, then rerun affected surfaces to emit schema-v6 records.",
|
|
253
|
+
}
|
|
254
|
+
]
|
|
255
|
+
elif records_available:
|
|
256
|
+
state = "records_ready"
|
|
257
|
+
summary_text = (
|
|
258
|
+
"current records verified; configured surfaces await record dependencies"
|
|
259
|
+
if surfaces_waiting
|
|
260
|
+
else "records ready and verified"
|
|
261
|
+
)
|
|
262
|
+
next_steps = [
|
|
263
|
+
{"command": command, "description": description}
|
|
264
|
+
for command, description in build_next_steps(
|
|
265
|
+
decl,
|
|
266
|
+
job_label=str(job_path),
|
|
267
|
+
runtime=runtime,
|
|
268
|
+
include_plot=plot_ready,
|
|
269
|
+
include_export=export_ready,
|
|
270
|
+
)
|
|
271
|
+
]
|
|
272
|
+
if surfaces_waiting:
|
|
273
|
+
next_steps.insert(
|
|
274
|
+
0,
|
|
275
|
+
{
|
|
276
|
+
"command": reader_command("run", job_path),
|
|
277
|
+
"description": "Produce the missing record dependencies before using configured plot or export surfaces.",
|
|
278
|
+
},
|
|
279
|
+
)
|
|
280
|
+
elif uncataloged_outputs_present:
|
|
281
|
+
state = "uncataloged_outputs_present"
|
|
282
|
+
summary_text = "generated outputs present without usable current records"
|
|
283
|
+
next_steps = [
|
|
284
|
+
{
|
|
285
|
+
"command": reader_command("run", job_path),
|
|
286
|
+
"description": "Rerun from source inputs to rebuild records and selected outputs.",
|
|
287
|
+
}
|
|
288
|
+
]
|
|
289
|
+
else:
|
|
290
|
+
state = "runnable"
|
|
291
|
+
summary_text = "ready to run"
|
|
292
|
+
next_steps = [
|
|
293
|
+
{
|
|
294
|
+
"command": reader_command("run", job_path),
|
|
295
|
+
"description": "Generate dataframe records and selected outputs.",
|
|
296
|
+
}
|
|
297
|
+
]
|
|
298
|
+
errors = deepcopy(summary["errors"])
|
|
299
|
+
if record_catalog_error is not None:
|
|
300
|
+
errors.append(f"records catalog: {record_catalog_error}")
|
|
301
|
+
if verification_report is not None and verification_report["status"] == "failed":
|
|
302
|
+
for issue in verification_report.get("issues", []):
|
|
303
|
+
errors.append(f"record verification: {issue['reason']}")
|
|
304
|
+
for record in verification_report.get("records", []):
|
|
305
|
+
for issue in record.get("issues", []):
|
|
306
|
+
errors.append(f"record verification ({record['record_id']}): {issue['reason']}")
|
|
307
|
+
return {
|
|
308
|
+
"state": state,
|
|
309
|
+
"summary": summary_text,
|
|
310
|
+
"preflight": {
|
|
311
|
+
"status": summary["status"],
|
|
312
|
+
"issues": len(errors),
|
|
313
|
+
"files": summary["files"]["summary"],
|
|
314
|
+
"dependencies": summary["dependencies"]["summary"],
|
|
315
|
+
},
|
|
316
|
+
"records": {
|
|
317
|
+
"catalog": records_catalog,
|
|
318
|
+
"available": records_available,
|
|
319
|
+
"verification": verification_report["status"] if verification_report else None,
|
|
320
|
+
"uncataloged_outputs_present": uncataloged_outputs_present,
|
|
321
|
+
},
|
|
322
|
+
"capabilities": {
|
|
323
|
+
"run": can_run,
|
|
324
|
+
"records": records_available,
|
|
325
|
+
"verify": records_catalog,
|
|
326
|
+
"plot": plot_ready and bool(workbench.plots),
|
|
327
|
+
"export": export_ready and bool(workbench.exports),
|
|
328
|
+
"notebook_scaffold": True,
|
|
329
|
+
"notebook_read_records": verified_records,
|
|
330
|
+
},
|
|
331
|
+
"errors": errors,
|
|
332
|
+
"next_steps": next_steps,
|
|
333
|
+
}
|