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,684 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
from dataclasses import replace
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from rich import box
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
|
|
12
|
+
from reader_workbench.errors import ConfigError, ExecutionError, ReaderError
|
|
13
|
+
from reader_workbench.plotting.mpl import ensure_mpl_cache_dir
|
|
14
|
+
from reader_workbench.runtime import ReaderRuntime, builtin_runtime
|
|
15
|
+
from reader_workbench.workbench.commands import reader_command
|
|
16
|
+
from reader_workbench.workbench.decl import WorkbenchDecl
|
|
17
|
+
from reader_workbench.workbench.experiment import ExperimentSemantics
|
|
18
|
+
from reader_workbench.workbench.graph import (
|
|
19
|
+
FileRef,
|
|
20
|
+
RecordCollectionRef,
|
|
21
|
+
RecordRef,
|
|
22
|
+
ResourceRef,
|
|
23
|
+
ensure_unique_workbench_ids,
|
|
24
|
+
input_ref_display,
|
|
25
|
+
resolve_workbench,
|
|
26
|
+
)
|
|
27
|
+
from reader_workbench.workbench.paths import resolve_path_within_root
|
|
28
|
+
from reader_workbench.workbench.records import DataFrameArtifactRecord, resolve_source_record
|
|
29
|
+
from reader_workbench.workbench.registry import Plugin, PreflightIssue
|
|
30
|
+
|
|
31
|
+
from ._shared import collect_categories
|
|
32
|
+
from .contracts import _resolve_output_labels
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _validate_reads_shape(*, label: str, step: Any, plugin_cls: Any) -> None:
|
|
36
|
+
try:
|
|
37
|
+
required = plugin_cls.input_ports()
|
|
38
|
+
expected = set(required)
|
|
39
|
+
mandatory = {name for name, port in required.items() if not port.optional}
|
|
40
|
+
provided = set((step.reads or {}).keys())
|
|
41
|
+
missing = sorted(mandatory - provided)
|
|
42
|
+
extra = sorted(provided - expected)
|
|
43
|
+
if not missing and not extra:
|
|
44
|
+
return
|
|
45
|
+
parts = []
|
|
46
|
+
if missing:
|
|
47
|
+
parts.append(f"missing inputs: {missing}")
|
|
48
|
+
if extra:
|
|
49
|
+
parts.append(f"unexpected inputs: {extra}")
|
|
50
|
+
raise ConfigError(f"{label} {step.id}: reads do not match plugin inputs ({'; '.join(parts)})")
|
|
51
|
+
except ConfigError:
|
|
52
|
+
raise
|
|
53
|
+
except Exception as err:
|
|
54
|
+
raise ConfigError(f"{label} {step.id}: could not validate reads: {err}") from err
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _assert_known_read_targets(*, label: str, step: Any, available_labels: set[str], plugin_cls: Any) -> None:
|
|
58
|
+
input_ports = plugin_cls.input_ports()
|
|
59
|
+
for key, target in (step.reads or {}).items():
|
|
60
|
+
port = input_ports.get(key)
|
|
61
|
+
if port is None:
|
|
62
|
+
continue
|
|
63
|
+
if port.kind in {"dataframe", "file_bundle"} and isinstance(target, FileRef | ResourceRef):
|
|
64
|
+
raise ConfigError(
|
|
65
|
+
f"{label} {step.id}: reads '{key}' targets '{input_ref_display(target)}', "
|
|
66
|
+
f"but the plugin expects a {port.kind} record."
|
|
67
|
+
)
|
|
68
|
+
if port.kind == "record_collection" and not isinstance(target, RecordCollectionRef):
|
|
69
|
+
raise ConfigError(f"{label} {step.id}: reads '{key}' must target a declared record collection.")
|
|
70
|
+
if isinstance(target, RecordCollectionRef):
|
|
71
|
+
if port.kind != "record_collection":
|
|
72
|
+
raise ConfigError(
|
|
73
|
+
f"{label} {step.id}: reads '{key}' targets a record collection, "
|
|
74
|
+
f"but the plugin expects {port.kind!r}."
|
|
75
|
+
)
|
|
76
|
+
continue
|
|
77
|
+
if port.kind in {"file_path", "file_set"} and isinstance(target, RecordRef):
|
|
78
|
+
raise ConfigError(
|
|
79
|
+
f"{label} {step.id}: reads '{key}' targets record '{target.record_id}', "
|
|
80
|
+
"but the plugin expects an explicit file/resource binding."
|
|
81
|
+
)
|
|
82
|
+
if isinstance(target, FileRef | ResourceRef):
|
|
83
|
+
continue
|
|
84
|
+
if isinstance(target, RecordRef) and target.record_id in available_labels:
|
|
85
|
+
continue
|
|
86
|
+
preview = sorted(available_labels)
|
|
87
|
+
shown = ", ".join(preview[:12]) if preview else "—"
|
|
88
|
+
tail = " …" if len(preview) > 12 else ""
|
|
89
|
+
source = "pipeline" if label in {"plot", "export"} else "any prior step"
|
|
90
|
+
raise ConfigError(
|
|
91
|
+
f"{label} {step.id}: reads '{key}' → '{input_ref_display(target)}', which is not produced by {source}. "
|
|
92
|
+
f"Known labels{' so far' if label == 'pipeline' else ''}: {shown}{tail}. "
|
|
93
|
+
"Check writes/reads aliases or use explicit file/resource bindings."
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _effective_step_with(*, protocol: Any, step: Any) -> dict[str, Any]:
|
|
98
|
+
return protocol.effective_plugin_config(plugin_id=step.plugin, step_with=(step.with_ or {}))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _validate_step_config(*, label: str, step: Any, plugin_cls: Any, protocol: Any) -> dict[str, Any]:
|
|
102
|
+
with_block = _effective_step_with(protocol=protocol, step=step)
|
|
103
|
+
try:
|
|
104
|
+
plugin_cls.ConfigModel.model_validate(with_block)
|
|
105
|
+
except Exception as err:
|
|
106
|
+
raise ConfigError(f"{label} {step.id}: invalid config for {step.plugin}: {err}") from err
|
|
107
|
+
return with_block
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _validate_plot_semantic_refs(*, step: Any, experiment: ExperimentSemantics, with_block: dict[str, Any]) -> None:
|
|
111
|
+
partition = with_block.get("partition")
|
|
112
|
+
if partition is not None:
|
|
113
|
+
try:
|
|
114
|
+
experiment.annotations.resolve_plot_partition(partition=partition)
|
|
115
|
+
except Exception as err:
|
|
116
|
+
raise ConfigError(f"plot {step.id}: invalid plot partition: {err}") from err
|
|
117
|
+
|
|
118
|
+
x_column = with_block.get("x")
|
|
119
|
+
y_column = with_block.get("y")
|
|
120
|
+
try:
|
|
121
|
+
experiment.annotations.resolve_order_arg(
|
|
122
|
+
order=with_block.get("order_x"),
|
|
123
|
+
order_ref=with_block.get("order_x_ref"),
|
|
124
|
+
column=(x_column if isinstance(x_column, str) else None),
|
|
125
|
+
arg_name="order_x",
|
|
126
|
+
)
|
|
127
|
+
experiment.annotations.resolve_order_arg(
|
|
128
|
+
order=with_block.get("order_y"),
|
|
129
|
+
order_ref=with_block.get("order_y_ref"),
|
|
130
|
+
column=(y_column if isinstance(y_column, str) else None),
|
|
131
|
+
arg_name="order_y",
|
|
132
|
+
)
|
|
133
|
+
if isinstance(with_block.get("state_map_ref"), str):
|
|
134
|
+
experiment.annotations.resolve_ordered_state_space(ref=with_block["state_map_ref"])
|
|
135
|
+
except Exception as err:
|
|
136
|
+
raise ConfigError(f"plot {step.id}: invalid ordering semantic reference: {err}") from err
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _validate_pipeline_semantic_refs(*, step: Any, experiment: ExperimentSemantics, with_block: dict[str, Any]) -> None:
|
|
140
|
+
try:
|
|
141
|
+
if isinstance(with_block.get("state_map_ref"), str):
|
|
142
|
+
experiment.annotations.resolve_ordered_state_space(ref=with_block["state_map_ref"])
|
|
143
|
+
except Exception as err:
|
|
144
|
+
raise ConfigError(f"pipeline {step.id}: invalid annotation semantic reference: {err}") from err
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _validate_pipeline_steps(
|
|
148
|
+
items: list[Any], *, registry: Any, experiment: ExperimentSemantics, protocol: Any
|
|
149
|
+
) -> set[str]:
|
|
150
|
+
available: set[str] = set()
|
|
151
|
+
produced: set[str] = set()
|
|
152
|
+
for step in items:
|
|
153
|
+
try:
|
|
154
|
+
plugin_cls = registry.resolve(step.plugin)
|
|
155
|
+
except Exception as err:
|
|
156
|
+
raise ConfigError(f"pipeline {step.id}: {err}") from err
|
|
157
|
+
_validate_reads_shape(label="pipeline", step=step, plugin_cls=plugin_cls)
|
|
158
|
+
_assert_known_read_targets(
|
|
159
|
+
label="pipeline",
|
|
160
|
+
step=step,
|
|
161
|
+
available_labels=available | produced,
|
|
162
|
+
plugin_cls=plugin_cls,
|
|
163
|
+
)
|
|
164
|
+
with_block = _validate_step_config(label="pipeline", step=step, plugin_cls=plugin_cls, protocol=protocol)
|
|
165
|
+
_validate_pipeline_semantic_refs(step=step, experiment=experiment, with_block=with_block)
|
|
166
|
+
try:
|
|
167
|
+
output_labels = _resolve_output_labels(
|
|
168
|
+
step_id=step.id,
|
|
169
|
+
output_ports=plugin_cls.output_ports(),
|
|
170
|
+
writes=(step.writes or {}),
|
|
171
|
+
)
|
|
172
|
+
except ExecutionError as err:
|
|
173
|
+
raise ConfigError(f"pipeline {step.id}: {err}") from err
|
|
174
|
+
for out_name, out_label in output_labels.items():
|
|
175
|
+
if plugin_cls.output_ports()[out_name].kind != "dataframe":
|
|
176
|
+
continue
|
|
177
|
+
if out_label.record_id in available or out_label.record_id in produced:
|
|
178
|
+
raise ConfigError(
|
|
179
|
+
f"pipeline {step.id}: output label '{out_label.record_id}' is already produced by another step. "
|
|
180
|
+
"Use a unique writes mapping to avoid clobbering dataframe records."
|
|
181
|
+
)
|
|
182
|
+
produced.add(out_label.record_id)
|
|
183
|
+
return available | produced
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _validate_specs(
|
|
187
|
+
items: list[Any],
|
|
188
|
+
*,
|
|
189
|
+
label: str,
|
|
190
|
+
registry: Any,
|
|
191
|
+
available_labels: set[str],
|
|
192
|
+
protocol: Any,
|
|
193
|
+
experiment: ExperimentSemantics | None = None,
|
|
194
|
+
) -> None:
|
|
195
|
+
for spec_item in items:
|
|
196
|
+
try:
|
|
197
|
+
plugin_cls = registry.resolve(spec_item.plugin)
|
|
198
|
+
except Exception as err:
|
|
199
|
+
raise ConfigError(f"{label} {spec_item.id}: {err}") from err
|
|
200
|
+
_validate_reads_shape(label=label, step=spec_item, plugin_cls=plugin_cls)
|
|
201
|
+
_assert_known_read_targets(
|
|
202
|
+
label=label,
|
|
203
|
+
step=spec_item,
|
|
204
|
+
available_labels=available_labels,
|
|
205
|
+
plugin_cls=plugin_cls,
|
|
206
|
+
)
|
|
207
|
+
with_block = _validate_step_config(label=label, step=spec_item, plugin_cls=plugin_cls, protocol=protocol)
|
|
208
|
+
if label == "plot":
|
|
209
|
+
if experiment is None:
|
|
210
|
+
raise ConfigError("plot validation requires experiment semantics")
|
|
211
|
+
_validate_plot_semantic_refs(
|
|
212
|
+
step=spec_item,
|
|
213
|
+
experiment=experiment,
|
|
214
|
+
with_block=with_block,
|
|
215
|
+
)
|
|
216
|
+
try:
|
|
217
|
+
cfg = plugin_cls.ConfigModel.model_validate(with_block)
|
|
218
|
+
plugin_cls.validate_semantic_references(experiment=experiment, cfg=cfg)
|
|
219
|
+
except Exception as err:
|
|
220
|
+
raise ConfigError(f"plot {spec_item.id}: invalid plugin semantic reference: {err}") from err
|
|
221
|
+
data_outputs = {
|
|
222
|
+
key: port.render() for key, port in plugin_cls.output_ports().items() if port.kind == "dataframe"
|
|
223
|
+
}
|
|
224
|
+
if data_outputs:
|
|
225
|
+
raise ConfigError(
|
|
226
|
+
f"{label} {spec_item.id}: plugins must not declare data outputs (got {data_outputs}). "
|
|
227
|
+
"Move data outputs into the pipeline."
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _configured_output_steps(*, protocol: Any) -> tuple[tuple[Any, ...], tuple[Any, ...]]:
|
|
232
|
+
"""Compile explicitly configured output blocks without selecting them for execution."""
|
|
233
|
+
|
|
234
|
+
outputs = deepcopy(protocol.outputs)
|
|
235
|
+
configured_ids: dict[str, tuple[str, ...]] = {"plots": (), "exports": ()}
|
|
236
|
+
output_keys = {"plots": "views", "exports": "artifacts"}
|
|
237
|
+
for section, config_key in output_keys.items():
|
|
238
|
+
block = protocol.outputs.get(section, {}) or {}
|
|
239
|
+
named_configs = block.get(config_key, {}) or {}
|
|
240
|
+
configured_ids[section] = tuple(named_configs)
|
|
241
|
+
if not configured_ids[section]:
|
|
242
|
+
continue
|
|
243
|
+
selected_block = dict(outputs.get(section, {}) or {})
|
|
244
|
+
selected_block["include"] = list(configured_ids[section])
|
|
245
|
+
selected_block["exclude"] = [
|
|
246
|
+
output_id for output_id in (selected_block.get("exclude") or ()) if output_id not in configured_ids[section]
|
|
247
|
+
]
|
|
248
|
+
if section == "plots":
|
|
249
|
+
selected_block["profile"] = "none"
|
|
250
|
+
outputs[section] = selected_block
|
|
251
|
+
if not any(configured_ids.values()):
|
|
252
|
+
return (), ()
|
|
253
|
+
|
|
254
|
+
plan = replace(protocol, outputs=outputs).compile()
|
|
255
|
+
configured: dict[str, tuple[Any, ...]] = {}
|
|
256
|
+
for section, config_key in output_keys.items():
|
|
257
|
+
steps = tuple(step for step in getattr(plan, section) if step.id in configured_ids[section])
|
|
258
|
+
for output_id in configured_ids[section]:
|
|
259
|
+
if sum(step.id == output_id for step in steps) != 1:
|
|
260
|
+
where = f"protocol.outputs.{section}.{config_key}.{output_id}"
|
|
261
|
+
raise ConfigError(f"{where} did not compile to exactly one plugin-backed output")
|
|
262
|
+
configured[section] = steps
|
|
263
|
+
return configured["plots"], configured["exports"]
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _validate_configured_output_steps(
|
|
267
|
+
*,
|
|
268
|
+
plot_steps: tuple[Any, ...],
|
|
269
|
+
export_steps: tuple[Any, ...],
|
|
270
|
+
registry: Any,
|
|
271
|
+
protocol: Any,
|
|
272
|
+
experiment: ExperimentSemantics,
|
|
273
|
+
) -> None:
|
|
274
|
+
for section, config_key, steps in (
|
|
275
|
+
("plots", "views", plot_steps),
|
|
276
|
+
("exports", "artifacts", export_steps),
|
|
277
|
+
):
|
|
278
|
+
for step in steps:
|
|
279
|
+
where = f"protocol.outputs.{section}.{config_key}.{step.id}"
|
|
280
|
+
try:
|
|
281
|
+
plugin_cls = registry.resolve(step.plugin)
|
|
282
|
+
except Exception as err:
|
|
283
|
+
raise ConfigError(f"{where}: {err}") from err
|
|
284
|
+
with_block = _effective_step_with(protocol=protocol, step=step)
|
|
285
|
+
try:
|
|
286
|
+
cfg = plugin_cls.ConfigModel.model_validate(with_block)
|
|
287
|
+
except Exception as err:
|
|
288
|
+
raise ConfigError(f"{where}: invalid config for {step.plugin}: {err}") from err
|
|
289
|
+
if section == "plots":
|
|
290
|
+
_validate_plot_semantic_refs(step=step, experiment=experiment, with_block=with_block)
|
|
291
|
+
try:
|
|
292
|
+
plugin_cls.validate_semantic_references(experiment=experiment, cfg=cfg)
|
|
293
|
+
except Exception as err:
|
|
294
|
+
raise ConfigError(f"{where}: invalid plugin semantic reference: {err}") from err
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _resolve_exp_path(path: Path, *, exp_root: Path | None) -> Path:
|
|
298
|
+
if exp_root is None:
|
|
299
|
+
return path
|
|
300
|
+
try:
|
|
301
|
+
return resolve_path_within_root(path, root=exp_root)
|
|
302
|
+
except ValueError as err:
|
|
303
|
+
raise ConfigError("Declared paths must stay under the experiment root after resolving symlinks.") from err
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _render_rel(path: Path, *, exp_root: Path | None) -> Path:
|
|
307
|
+
if exp_root is None:
|
|
308
|
+
return path
|
|
309
|
+
try:
|
|
310
|
+
return path.relative_to(exp_root)
|
|
311
|
+
except Exception:
|
|
312
|
+
return path
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _plugin_preflight_issues(
|
|
316
|
+
*,
|
|
317
|
+
items: list[Any],
|
|
318
|
+
label: str,
|
|
319
|
+
registry: Any,
|
|
320
|
+
protocol: Any,
|
|
321
|
+
exp_root: Path | None,
|
|
322
|
+
) -> list[tuple[str, str, PreflightIssue]]:
|
|
323
|
+
if exp_root is None:
|
|
324
|
+
return []
|
|
325
|
+
issues: list[tuple[str, str, PreflightIssue]] = []
|
|
326
|
+
for step in items:
|
|
327
|
+
plugin_cls = registry.resolve(step.plugin)
|
|
328
|
+
if getattr(plugin_cls.preflight_readiness, "__func__", plugin_cls.preflight_readiness) is getattr(
|
|
329
|
+
Plugin.preflight_readiness,
|
|
330
|
+
"__func__",
|
|
331
|
+
Plugin.preflight_readiness,
|
|
332
|
+
):
|
|
333
|
+
continue
|
|
334
|
+
cfg = plugin_cls.ConfigModel.model_validate(_effective_step_with(protocol=protocol, step=step))
|
|
335
|
+
for issue in plugin_cls.preflight_readiness(exp_dir=exp_root, cfg=cfg, reads=(step.reads or {})):
|
|
336
|
+
issues.append((label, step.id, issue))
|
|
337
|
+
return issues
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _source_record_preflight_issues(
|
|
341
|
+
*,
|
|
342
|
+
items: list[Any],
|
|
343
|
+
registry: Any,
|
|
344
|
+
contracts: Any,
|
|
345
|
+
) -> tuple[int, list[str]]:
|
|
346
|
+
declared = 0
|
|
347
|
+
issues: list[str] = []
|
|
348
|
+
for step in items:
|
|
349
|
+
plugin_cls = registry.resolve(step.plugin)
|
|
350
|
+
ports = plugin_cls.input_ports()
|
|
351
|
+
for label, ref in (step.reads or {}).items():
|
|
352
|
+
if not isinstance(ref, RecordCollectionRef):
|
|
353
|
+
continue
|
|
354
|
+
declared += len(ref.records)
|
|
355
|
+
if not ref.records:
|
|
356
|
+
issues.append(f"pipeline:{step.id} • {label} → no source records declared")
|
|
357
|
+
continue
|
|
358
|
+
expected_contract = ports[label].contract
|
|
359
|
+
for source_ref in ref.records:
|
|
360
|
+
source_label = f"{source_ref.experiment_id}:{source_ref.record_id}"
|
|
361
|
+
try:
|
|
362
|
+
resolved = resolve_source_record(source_ref, contracts=contracts)
|
|
363
|
+
record = resolved.record
|
|
364
|
+
if not isinstance(record, DataFrameArtifactRecord):
|
|
365
|
+
raise ConfigError(f"source record is {record.kind!r}, not a dataframe artifact")
|
|
366
|
+
if expected_contract is not None and not contracts.satisfies(
|
|
367
|
+
actual=record.contract_id,
|
|
368
|
+
expected=expected_contract,
|
|
369
|
+
):
|
|
370
|
+
raise ConfigError(f"contract {record.contract_id!r} does not satisfy {expected_contract!r}")
|
|
371
|
+
resolved.verify_artifact_integrity()
|
|
372
|
+
except (OSError, ReaderError) as exc:
|
|
373
|
+
issues.append(f"pipeline:{step.id} • {label} → {source_label}: {exc}")
|
|
374
|
+
return declared, issues
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _planned_output_record_ids(
|
|
378
|
+
*,
|
|
379
|
+
pipeline_items: list[Any],
|
|
380
|
+
plot_items: list[Any],
|
|
381
|
+
export_items: list[Any],
|
|
382
|
+
registry: Any,
|
|
383
|
+
) -> set[str]:
|
|
384
|
+
planned = {f"plot:{step.id}" for step in plot_items}
|
|
385
|
+
planned.update(f"export:{step.id}" for step in export_items)
|
|
386
|
+
for step in pipeline_items:
|
|
387
|
+
plugin_cls = registry.resolve(step.plugin)
|
|
388
|
+
planned.update(
|
|
389
|
+
ref.record_id
|
|
390
|
+
for ref in _resolve_output_labels(
|
|
391
|
+
step_id=step.id,
|
|
392
|
+
output_ports=plugin_cls.output_ports(),
|
|
393
|
+
writes=(step.writes or {}),
|
|
394
|
+
).values()
|
|
395
|
+
)
|
|
396
|
+
return planned
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _assert_no_source_record_output_collisions(
|
|
400
|
+
*,
|
|
401
|
+
items: list[Any],
|
|
402
|
+
planned_record_ids: set[str],
|
|
403
|
+
experiment_root: Path,
|
|
404
|
+
) -> None:
|
|
405
|
+
for step in items:
|
|
406
|
+
for label, ref in (step.reads or {}).items():
|
|
407
|
+
if not isinstance(ref, RecordCollectionRef):
|
|
408
|
+
continue
|
|
409
|
+
for source_ref in ref.records:
|
|
410
|
+
if source_ref.experiment_root == experiment_root and source_ref.record_id in planned_record_ids:
|
|
411
|
+
source_label = f"{source_ref.experiment_id}:{source_ref.record_id}"
|
|
412
|
+
raise ConfigError(
|
|
413
|
+
f"pipeline {step.id}: reads '{label}' source record {source_label!r} targets the same "
|
|
414
|
+
f"experiment and collides with planned output {source_ref.record_id!r}. Choose a source "
|
|
415
|
+
"owned by another experiment or a distinct output record id."
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def validation_summary(
|
|
420
|
+
decl: WorkbenchDecl,
|
|
421
|
+
*,
|
|
422
|
+
check_files: bool = False,
|
|
423
|
+
exp_root: Path | None = None,
|
|
424
|
+
runtime: ReaderRuntime | None = None,
|
|
425
|
+
plot_specs_override: list[Any] | None = None,
|
|
426
|
+
export_specs_override: list[Any] | None = None,
|
|
427
|
+
) -> dict[str, Any]:
|
|
428
|
+
runtime = runtime or builtin_runtime()
|
|
429
|
+
workbench = resolve_workbench(decl)
|
|
430
|
+
pipeline_steps = list(workbench.pipeline)
|
|
431
|
+
plot_specs = list(workbench.plots) if plot_specs_override is None else list(plot_specs_override)
|
|
432
|
+
export_specs = list(workbench.exports) if export_specs_override is None else list(export_specs_override)
|
|
433
|
+
ensure_unique_workbench_ids(pipeline_steps, plot_specs, export_specs)
|
|
434
|
+
bound_protocol = runtime.bind_protocol(decl.experiment_semantics.protocol)
|
|
435
|
+
configured_plot_steps, configured_export_steps = _configured_output_steps(protocol=bound_protocol)
|
|
436
|
+
categories = collect_categories([*workbench.plugin_steps(), *configured_plot_steps, *configured_export_steps])
|
|
437
|
+
if "plot" in categories:
|
|
438
|
+
ensure_mpl_cache_dir()
|
|
439
|
+
registry = runtime.plugins if categories else None
|
|
440
|
+
|
|
441
|
+
experiment_semantics = decl.experiment_semantics
|
|
442
|
+
pipeline_labels = _validate_pipeline_steps(
|
|
443
|
+
pipeline_steps,
|
|
444
|
+
registry=registry,
|
|
445
|
+
experiment=experiment_semantics,
|
|
446
|
+
protocol=bound_protocol,
|
|
447
|
+
)
|
|
448
|
+
if registry is not None:
|
|
449
|
+
planned_record_ids = _planned_output_record_ids(
|
|
450
|
+
pipeline_items=pipeline_steps,
|
|
451
|
+
plot_items=plot_specs,
|
|
452
|
+
export_items=export_specs,
|
|
453
|
+
registry=registry,
|
|
454
|
+
)
|
|
455
|
+
_assert_no_source_record_output_collisions(
|
|
456
|
+
items=pipeline_steps,
|
|
457
|
+
planned_record_ids=planned_record_ids,
|
|
458
|
+
experiment_root=decl.experiment.root,
|
|
459
|
+
)
|
|
460
|
+
if plot_specs:
|
|
461
|
+
if registry is None:
|
|
462
|
+
raise ConfigError("plot validation requires plugin-backed workbench specs")
|
|
463
|
+
_validate_specs(
|
|
464
|
+
plot_specs,
|
|
465
|
+
label="plot",
|
|
466
|
+
registry=registry,
|
|
467
|
+
available_labels=pipeline_labels,
|
|
468
|
+
protocol=bound_protocol,
|
|
469
|
+
experiment=experiment_semantics,
|
|
470
|
+
)
|
|
471
|
+
if export_specs:
|
|
472
|
+
if registry is None:
|
|
473
|
+
raise ConfigError("export validation requires plugin-backed workbench specs")
|
|
474
|
+
_validate_specs(
|
|
475
|
+
export_specs,
|
|
476
|
+
label="export",
|
|
477
|
+
registry=registry,
|
|
478
|
+
available_labels=pipeline_labels,
|
|
479
|
+
protocol=bound_protocol,
|
|
480
|
+
)
|
|
481
|
+
configured_plot_steps = tuple(step for step in configured_plot_steps if step.id not in {s.id for s in plot_specs})
|
|
482
|
+
configured_export_steps = tuple(
|
|
483
|
+
step for step in configured_export_steps if step.id not in {s.id for s in export_specs}
|
|
484
|
+
)
|
|
485
|
+
if configured_plot_steps or configured_export_steps:
|
|
486
|
+
if registry is None:
|
|
487
|
+
raise ConfigError("configured output validation requires plugin-backed workbench specs")
|
|
488
|
+
_validate_configured_output_steps(
|
|
489
|
+
plot_steps=configured_plot_steps,
|
|
490
|
+
export_steps=configured_export_steps,
|
|
491
|
+
registry=registry,
|
|
492
|
+
protocol=bound_protocol,
|
|
493
|
+
experiment=experiment_semantics,
|
|
494
|
+
)
|
|
495
|
+
declared_entries: list[tuple[str, str, str, Path]] = []
|
|
496
|
+
declared_roots: list[tuple[str, str, Path]] = []
|
|
497
|
+
if exp_root is not None:
|
|
498
|
+
for label, items in (("pipeline", pipeline_steps), ("plot", plot_specs), ("export", export_specs)):
|
|
499
|
+
for step in items:
|
|
500
|
+
for key, target in (step.reads or {}).items():
|
|
501
|
+
if isinstance(target, FileRef | ResourceRef):
|
|
502
|
+
declared_entries.append(
|
|
503
|
+
(label, step.id, key, _resolve_exp_path(target.path, exp_root=exp_root))
|
|
504
|
+
)
|
|
505
|
+
with_block = _effective_step_with(protocol=bound_protocol, step=step) if hasattr(step, "with_") else {}
|
|
506
|
+
auto_roots = with_block.get("auto_roots")
|
|
507
|
+
if isinstance(auto_roots, list):
|
|
508
|
+
for root in auto_roots:
|
|
509
|
+
declared_roots.append((label, step.id, _resolve_exp_path(Path(root), exp_root=exp_root)))
|
|
510
|
+
|
|
511
|
+
files_checked = None
|
|
512
|
+
file_issues: list[str] = []
|
|
513
|
+
dependency_issues: list[str] = []
|
|
514
|
+
source_record_issues: list[str] = []
|
|
515
|
+
source_records_declared = 0
|
|
516
|
+
readiness_errors: list[str] = []
|
|
517
|
+
if check_files:
|
|
518
|
+
files_checked = (len(declared_entries), len(declared_roots))
|
|
519
|
+
for label, step_id, key, path in declared_entries:
|
|
520
|
+
if not path.exists():
|
|
521
|
+
file_issues.append(f"{label}:{step_id} • {key} → {_render_rel(path, exp_root=exp_root)}")
|
|
522
|
+
for label, step_id, root in declared_roots:
|
|
523
|
+
if not root.exists():
|
|
524
|
+
file_issues.append(f"{label}:{step_id} • auto_roots → {_render_rel(root, exp_root=exp_root)}")
|
|
525
|
+
if registry is not None:
|
|
526
|
+
source_records_declared, source_record_issues = _source_record_preflight_issues(
|
|
527
|
+
items=pipeline_steps,
|
|
528
|
+
registry=registry,
|
|
529
|
+
contracts=runtime.contracts,
|
|
530
|
+
)
|
|
531
|
+
for label, step_id, issue in (
|
|
532
|
+
_plugin_preflight_issues(
|
|
533
|
+
items=pipeline_steps,
|
|
534
|
+
label="pipeline",
|
|
535
|
+
registry=registry,
|
|
536
|
+
protocol=bound_protocol,
|
|
537
|
+
exp_root=exp_root,
|
|
538
|
+
)
|
|
539
|
+
+ _plugin_preflight_issues(
|
|
540
|
+
items=plot_specs,
|
|
541
|
+
label="plot",
|
|
542
|
+
registry=registry,
|
|
543
|
+
protocol=bound_protocol,
|
|
544
|
+
exp_root=exp_root,
|
|
545
|
+
)
|
|
546
|
+
+ _plugin_preflight_issues(
|
|
547
|
+
items=export_specs,
|
|
548
|
+
label="export",
|
|
549
|
+
registry=registry,
|
|
550
|
+
protocol=bound_protocol,
|
|
551
|
+
exp_root=exp_root,
|
|
552
|
+
)
|
|
553
|
+
):
|
|
554
|
+
rendered = f"{label}:{step_id} • {issue.message}"
|
|
555
|
+
if issue.kind == "dependency":
|
|
556
|
+
dependency_issues.append(rendered)
|
|
557
|
+
else:
|
|
558
|
+
file_issues.append(rendered)
|
|
559
|
+
readiness_errors = [*file_issues, *dependency_issues, *source_record_issues]
|
|
560
|
+
|
|
561
|
+
file_total, root_total = files_checked or (len(declared_entries), len(declared_roots))
|
|
562
|
+
if not check_files:
|
|
563
|
+
files_payload = {
|
|
564
|
+
"mode": "skipped",
|
|
565
|
+
"checked": False,
|
|
566
|
+
"declared": {"file_inputs": file_total, "auto_roots": root_total},
|
|
567
|
+
"issues": 0,
|
|
568
|
+
"summary": "skipped (--no-files)",
|
|
569
|
+
}
|
|
570
|
+
elif file_total == 0 and root_total == 0 and not file_issues:
|
|
571
|
+
files_payload = {
|
|
572
|
+
"mode": "none_declared",
|
|
573
|
+
"checked": True,
|
|
574
|
+
"declared": {"file_inputs": 0, "auto_roots": 0},
|
|
575
|
+
"issues": 0,
|
|
576
|
+
"summary": "none declared",
|
|
577
|
+
}
|
|
578
|
+
else:
|
|
579
|
+
parts = []
|
|
580
|
+
if file_total:
|
|
581
|
+
parts.append(f"{file_total} file input(s)")
|
|
582
|
+
if root_total:
|
|
583
|
+
parts.append(f"{root_total} auto_root(s)")
|
|
584
|
+
if file_issues:
|
|
585
|
+
parts.append(f"{len(file_issues)} issue(s)")
|
|
586
|
+
files_payload = {
|
|
587
|
+
"mode": "error" if file_issues else "ok",
|
|
588
|
+
"checked": True,
|
|
589
|
+
"declared": {"file_inputs": file_total, "auto_roots": root_total},
|
|
590
|
+
"issues": len(file_issues),
|
|
591
|
+
"summary": (f"error ({', '.join(parts)})" if file_issues else f"ok ({', '.join(parts)})"),
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if not check_files:
|
|
595
|
+
dependencies_payload = {
|
|
596
|
+
"checked": False,
|
|
597
|
+
"issues": 0,
|
|
598
|
+
"summary": "skipped (--no-files)",
|
|
599
|
+
}
|
|
600
|
+
else:
|
|
601
|
+
dependencies_payload = {
|
|
602
|
+
"checked": True,
|
|
603
|
+
"issues": len(dependency_issues),
|
|
604
|
+
"summary": (f"error ({len(dependency_issues)} issue(s))" if dependency_issues else "ok"),
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
source_records_payload = {
|
|
608
|
+
"checked": check_files,
|
|
609
|
+
"declared": source_records_declared,
|
|
610
|
+
"issues": len(source_record_issues),
|
|
611
|
+
"summary": (
|
|
612
|
+
"skipped (--no-files)"
|
|
613
|
+
if not check_files
|
|
614
|
+
else (
|
|
615
|
+
f"error ({len(source_record_issues)} issue(s))"
|
|
616
|
+
if source_record_issues
|
|
617
|
+
else f"ok ({source_records_declared} record(s))"
|
|
618
|
+
)
|
|
619
|
+
),
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
checks = [
|
|
623
|
+
"schema",
|
|
624
|
+
"plugin availability",
|
|
625
|
+
"reads",
|
|
626
|
+
"output labels",
|
|
627
|
+
"plugin config",
|
|
628
|
+
]
|
|
629
|
+
if check_files:
|
|
630
|
+
checks.extend(("runtime readiness", "source record revisions"))
|
|
631
|
+
|
|
632
|
+
return {
|
|
633
|
+
"status": "error" if readiness_errors else "ok",
|
|
634
|
+
"protocol": decl.experiment_semantics.protocol.id,
|
|
635
|
+
"counts": {
|
|
636
|
+
"pipeline": len(pipeline_steps),
|
|
637
|
+
"plots": len(plot_specs),
|
|
638
|
+
"exports": len(export_specs),
|
|
639
|
+
},
|
|
640
|
+
"checks": checks,
|
|
641
|
+
"files": files_payload,
|
|
642
|
+
"dependencies": dependencies_payload,
|
|
643
|
+
"source_records": source_records_payload,
|
|
644
|
+
"errors": readiness_errors,
|
|
645
|
+
"tip": (
|
|
646
|
+
f"fix readiness issues or use '{reader_command('validate', '--no-files')}' for config-only checks"
|
|
647
|
+
if readiness_errors
|
|
648
|
+
else "use 'reader explain' to see inputs/outputs"
|
|
649
|
+
),
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def validate(
|
|
654
|
+
decl: WorkbenchDecl,
|
|
655
|
+
*,
|
|
656
|
+
console: Console,
|
|
657
|
+
check_files: bool = False,
|
|
658
|
+
exp_root: Path | None = None,
|
|
659
|
+
runtime: ReaderRuntime | None = None,
|
|
660
|
+
) -> dict[str, Any]:
|
|
661
|
+
summary = validation_summary(
|
|
662
|
+
decl,
|
|
663
|
+
check_files=check_files,
|
|
664
|
+
exp_root=exp_root,
|
|
665
|
+
runtime=runtime,
|
|
666
|
+
)
|
|
667
|
+
ok = summary["status"] == "ok"
|
|
668
|
+
lines = [
|
|
669
|
+
"[green]✓ Config validated[/green]" if ok else "[error]✗ Validation failed[/error]",
|
|
670
|
+
f"[dim]protocol[/dim]: {summary['protocol']}",
|
|
671
|
+
f"[dim]pipeline[/dim]: {summary['counts']['pipeline']}",
|
|
672
|
+
f"[dim]plots[/dim]: {summary['counts']['plots']}",
|
|
673
|
+
f"[dim]exports[/dim]: {summary['counts']['exports']}",
|
|
674
|
+
f"[dim]checks[/dim]: {', '.join(summary['checks'])}",
|
|
675
|
+
]
|
|
676
|
+
lines.append(f"[dim]files[/dim]: {summary['files']['summary']}")
|
|
677
|
+
lines.append(f"[dim]dependencies[/dim]: {summary['dependencies']['summary']}")
|
|
678
|
+
lines.append(f"[dim]source records[/dim]: {summary['source_records']['summary']}")
|
|
679
|
+
if summary["errors"]:
|
|
680
|
+
lines.append("[dim]errors[/dim]:")
|
|
681
|
+
lines.extend(f"- {item}" for item in summary["errors"])
|
|
682
|
+
lines.append(f"[dim]tip[/dim]: {summary['tip']}")
|
|
683
|
+
console.print(Panel.fit("\n".join(lines), border_style=("green" if ok else "error"), box=box.ROUNDED))
|
|
684
|
+
return summary
|