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.
Files changed (293) hide show
  1. reader_workbench/__init__.py +22 -0
  2. reader_workbench/__main__.py +4 -0
  3. reader_workbench/_version.py +17 -0
  4. reader_workbench/api/__init__.py +74 -0
  5. reader_workbench/api/_record_reads.py +75 -0
  6. reader_workbench/api/artifacts.py +79 -0
  7. reader_workbench/api/facade.py +538 -0
  8. reader_workbench/api/models.py +285 -0
  9. reader_workbench/api/notebooks.py +63 -0
  10. reader_workbench/contracts/__init__.py +18 -0
  11. reader_workbench/contracts/builtins/__init__.py +36 -0
  12. reader_workbench/contracts/builtins/cytometry.py +140 -0
  13. reader_workbench/contracts/builtins/four_state_event_window.py +214 -0
  14. reader_workbench/contracts/builtins/generic.py +21 -0
  15. reader_workbench/contracts/builtins/logic.py +149 -0
  16. reader_workbench/contracts/builtins/plate_reader.py +47 -0
  17. reader_workbench/contracts/catalog.py +257 -0
  18. reader_workbench/contracts/model.py +109 -0
  19. reader_workbench/domains/__init__.py +1 -0
  20. reader_workbench/domains/cytometry/__init__.py +3 -0
  21. reader_workbench/domains/cytometry/analysis/__init__.py +29 -0
  22. reader_workbench/domains/cytometry/analysis/events.py +182 -0
  23. reader_workbench/domains/cytometry/analysis/gating.py +175 -0
  24. reader_workbench/domains/cytometry/analysis/workflow.py +248 -0
  25. reader_workbench/domains/cytometry/io/__init__.py +3 -0
  26. reader_workbench/domains/cytometry/io/fcs.py +135 -0
  27. reader_workbench/domains/cytometry/plots/__init__.py +5 -0
  28. reader_workbench/domains/cytometry/plots/diagnostic.py +155 -0
  29. reader_workbench/domains/logic/__init__.py +3 -0
  30. reader_workbench/domains/logic/crosstalk/__init__.py +3 -0
  31. reader_workbench/domains/logic/crosstalk/pairs.py +661 -0
  32. reader_workbench/domains/logic/four_state_vector/__init__.py +7 -0
  33. reader_workbench/domains/logic/four_state_vector/builder.py +321 -0
  34. reader_workbench/domains/logic/four_state_vector/collection/__init__.py +20 -0
  35. reader_workbench/domains/logic/four_state_vector/collection/checks.py +49 -0
  36. reader_workbench/domains/logic/four_state_vector/collection/constants.py +29 -0
  37. reader_workbench/domains/logic/four_state_vector/collection/model.py +19 -0
  38. reader_workbench/domains/logic/four_state_vector/collection/render.py +383 -0
  39. reader_workbench/domains/logic/four_state_vector/collection/sources.py +185 -0
  40. reader_workbench/domains/logic/four_state_vector/config.py +214 -0
  41. reader_workbench/domains/logic/four_state_vector/diagnostic.py +361 -0
  42. reader_workbench/domains/logic/four_state_vector/heatmap.py +86 -0
  43. reader_workbench/domains/logic/four_state_vector/math.py +191 -0
  44. reader_workbench/domains/logic/four_state_vector/reference.py +85 -0
  45. reader_workbench/domains/logic/four_state_vector/selection.py +228 -0
  46. reader_workbench/domains/logic/four_state_vector/treatment_semantics.py +51 -0
  47. reader_workbench/domains/logic/four_state_vector/validation.py +19 -0
  48. reader_workbench/domains/logic/logic_symmetry/__init__.py +3 -0
  49. reader_workbench/domains/logic/logic_symmetry/encodings.py +93 -0
  50. reader_workbench/domains/logic/logic_symmetry/extract_corners.py +192 -0
  51. reader_workbench/domains/logic/logic_symmetry/main.py +236 -0
  52. reader_workbench/domains/logic/logic_symmetry/metrics.py +96 -0
  53. reader_workbench/domains/logic/logic_symmetry/overlay.py +129 -0
  54. reader_workbench/domains/logic/logic_symmetry/prep.py +138 -0
  55. reader_workbench/domains/logic/logic_symmetry/render.py +356 -0
  56. reader_workbench/domains/logic/treatment_columns.py +42 -0
  57. reader_workbench/domains/plate_reader/__init__.py +1 -0
  58. reader_workbench/domains/plate_reader/analysis/__init__.py +14 -0
  59. reader_workbench/domains/plate_reader/analysis/fold_change.py +474 -0
  60. reader_workbench/domains/plate_reader/analysis/four_state_event_window/__init__.py +21 -0
  61. reader_workbench/domains/plate_reader/analysis/four_state_event_window/aggregation.py +191 -0
  62. reader_workbench/domains/plate_reader/analysis/four_state_event_window/contract_fields.py +50 -0
  63. reader_workbench/domains/plate_reader/analysis/four_state_event_window/contracts.py +320 -0
  64. reader_workbench/domains/plate_reader/analysis/four_state_event_window/design_dispositions.py +54 -0
  65. reader_workbench/domains/plate_reader/analysis/four_state_event_window/disposition_records.py +140 -0
  66. reader_workbench/domains/plate_reader/analysis/four_state_event_window/event_sensitivity.py +27 -0
  67. reader_workbench/domains/plate_reader/analysis/four_state_event_window/materialize.py +274 -0
  68. reader_workbench/domains/plate_reader/analysis/four_state_event_window/observation_resampling.py +97 -0
  69. reader_workbench/domains/plate_reader/analysis/four_state_event_window/reduction.py +62 -0
  70. reader_workbench/domains/plate_reader/analysis/four_state_event_window/seeds.py +15 -0
  71. reader_workbench/domains/plate_reader/analysis/four_state_event_window/sources.py +308 -0
  72. reader_workbench/domains/plate_reader/analysis/four_state_event_window/well_exclusion_validation.py +94 -0
  73. reader_workbench/domains/plate_reader/analysis/four_state_event_window/well_exclusions.py +54 -0
  74. reader_workbench/domains/plate_reader/analysis/timepoints.py +76 -0
  75. reader_workbench/domains/plate_reader/io/__init__.py +6 -0
  76. reader_workbench/domains/plate_reader/io/sample_map.py +65 -0
  77. reader_workbench/domains/plate_reader/io/synergy_h1/__init__.py +6 -0
  78. reader_workbench/domains/plate_reader/io/synergy_h1/_kinetic.py +141 -0
  79. reader_workbench/domains/plate_reader/io/synergy_h1/_parser.py +295 -0
  80. reader_workbench/domains/plate_reader/io/synergy_h1/_shared.py +195 -0
  81. reader_workbench/domains/plate_reader/io/synergy_h1/_snapshot.py +130 -0
  82. reader_workbench/domains/plate_reader/ordering.py +59 -0
  83. reader_workbench/domains/plate_reader/plots/__init__.py +15 -0
  84. reader_workbench/domains/plate_reader/plots/_data.py +29 -0
  85. reader_workbench/domains/plate_reader/plots/common.py +346 -0
  86. reader_workbench/domains/plate_reader/plots/distributions.py +324 -0
  87. reader_workbench/domains/plate_reader/plots/dual_reporter_triptych.py +525 -0
  88. reader_workbench/domains/plate_reader/plots/dual_reporter_triptych_render.py +195 -0
  89. reader_workbench/domains/plate_reader/plots/four_state_event_window/__init__.py +26 -0
  90. reader_workbench/domains/plate_reader/plots/four_state_event_window/diagnostic.py +295 -0
  91. reader_workbench/domains/plate_reader/plots/four_state_event_window/diagnostic_components.py +149 -0
  92. reader_workbench/domains/plate_reader/plots/four_state_event_window/diagnostic_render.py +308 -0
  93. reader_workbench/domains/plate_reader/plots/four_state_event_window/diagnostic_style.py +51 -0
  94. reader_workbench/domains/plate_reader/plots/four_state_event_window/schema.py +8 -0
  95. reader_workbench/domains/plate_reader/plots/four_state_event_window/summary.py +140 -0
  96. reader_workbench/domains/plate_reader/plots/grouping.py +53 -0
  97. reader_workbench/domains/plate_reader/plots/panels/__init__.py +12 -0
  98. reader_workbench/domains/plate_reader/plots/panels/snapshot.py +161 -0
  99. reader_workbench/domains/plate_reader/plots/panels/snapshot_data.py +91 -0
  100. reader_workbench/domains/plate_reader/plots/panels/time_series.py +296 -0
  101. reader_workbench/domains/plate_reader/plots/single_reporter_diagnostic.py +435 -0
  102. reader_workbench/domains/plate_reader/plots/single_reporter_diagnostic_render.py +300 -0
  103. reader_workbench/domains/plate_reader/plots/snapshot_barplot/__init__.py +315 -0
  104. reader_workbench/domains/plate_reader/plots/snapshot_barplot/planning.py +168 -0
  105. reader_workbench/domains/plate_reader/plots/snapshot_heatmap/__init__.py +205 -0
  106. reader_workbench/domains/plate_reader/plots/snapshot_heatmap/inputs.py +103 -0
  107. reader_workbench/domains/plate_reader/plots/time_series.py +317 -0
  108. reader_workbench/domains/plate_reader/plots/ts_and_snap/__init__.py +479 -0
  109. reader_workbench/domains/plate_reader/plots/ts_and_snap/planning.py +283 -0
  110. reader_workbench/domains/time_series/__init__.py +29 -0
  111. reader_workbench/domains/time_series/aggregation.py +60 -0
  112. reader_workbench/domains/time_series/contracts.py +368 -0
  113. reader_workbench/domains/time_series/reduction.py +395 -0
  114. reader_workbench/errors.py +55 -0
  115. reader_workbench/maintenance/__init__.py +6 -0
  116. reader_workbench/maintenance/docs.py +335 -0
  117. reader_workbench/maintenance/model.py +28 -0
  118. reader_workbench/maintenance/release.py +39 -0
  119. reader_workbench/maintenance/skills.py +124 -0
  120. reader_workbench/plotting/__init__.py +20 -0
  121. reader_workbench/plotting/mpl.py +56 -0
  122. reader_workbench/plotting/sinks.py +69 -0
  123. reader_workbench/plotting/style.py +175 -0
  124. reader_workbench/plotting/utils.py +27 -0
  125. reader_workbench/plugins/__init__.py +1 -0
  126. reader_workbench/plugins/catalog.py +33 -0
  127. reader_workbench/plugins/export/__init__.py +0 -0
  128. reader_workbench/plugins/export/_paths.py +21 -0
  129. reader_workbench/plugins/export/csv.py +41 -0
  130. reader_workbench/plugins/export/xlsx.py +44 -0
  131. reader_workbench/plugins/ingest/__init__.py +0 -0
  132. reader_workbench/plugins/ingest/_discovery.py +58 -0
  133. reader_workbench/plugins/ingest/discovery_policy.py +66 -0
  134. reader_workbench/plugins/ingest/flow_cytometer.py +139 -0
  135. reader_workbench/plugins/ingest/synergy_h1.py +234 -0
  136. reader_workbench/plugins/manifests/__init__.py +1 -0
  137. reader_workbench/plugins/manifests/export.py +29 -0
  138. reader_workbench/plugins/manifests/ingest.py +29 -0
  139. reader_workbench/plugins/manifests/plot.py +161 -0
  140. reader_workbench/plugins/manifests/transform.py +172 -0
  141. reader_workbench/plugins/manifests/validator.py +18 -0
  142. reader_workbench/plugins/plot/__init__.py +0 -0
  143. reader_workbench/plugins/plot/_shared.py +55 -0
  144. reader_workbench/plugins/plot/cytometry_diagnostic.py +54 -0
  145. reader_workbench/plugins/plot/distributions.py +58 -0
  146. reader_workbench/plugins/plot/dual_reporter_triptych.py +224 -0
  147. reader_workbench/plugins/plot/four_state_event_window_diagnostic.py +133 -0
  148. reader_workbench/plugins/plot/four_state_event_window_summary.py +53 -0
  149. reader_workbench/plugins/plot/four_state_vector_collection.py +45 -0
  150. reader_workbench/plugins/plot/four_state_vector_diagnostic.py +105 -0
  151. reader_workbench/plugins/plot/four_state_vector_heatmap.py +63 -0
  152. reader_workbench/plugins/plot/logic_symmetry.py +56 -0
  153. reader_workbench/plugins/plot/single_reporter_diagnostic.py +279 -0
  154. reader_workbench/plugins/plot/snapshot_barplot.py +65 -0
  155. reader_workbench/plugins/plot/snapshot_heatmap.py +104 -0
  156. reader_workbench/plugins/plot/time_series.py +114 -0
  157. reader_workbench/plugins/plot/ts_and_snap.py +210 -0
  158. reader_workbench/plugins/transform/__init__.py +0 -0
  159. reader_workbench/plugins/transform/_four_state_vector.py +204 -0
  160. reader_workbench/plugins/transform/_labeling.py +109 -0
  161. reader_workbench/plugins/transform/alias.py +70 -0
  162. reader_workbench/plugins/transform/assay_labels.py +62 -0
  163. reader_workbench/plugins/transform/blank.py +79 -0
  164. reader_workbench/plugins/transform/crosstalk_pairs.py +180 -0
  165. reader_workbench/plugins/transform/cytometry_gating.py +120 -0
  166. reader_workbench/plugins/transform/fold_change.py +79 -0
  167. reader_workbench/plugins/transform/four_state_event_window.py +93 -0
  168. reader_workbench/plugins/transform/four_state_vector.py +62 -0
  169. reader_workbench/plugins/transform/four_state_vector_collection.py +41 -0
  170. reader_workbench/plugins/transform/logic_symmetry.py +67 -0
  171. reader_workbench/plugins/transform/outlier_filter.py +60 -0
  172. reader_workbench/plugins/transform/overflow.py +197 -0
  173. reader_workbench/plugins/transform/ratio.py +237 -0
  174. reader_workbench/plugins/transform/sample_map.py +170 -0
  175. reader_workbench/plugins/transform/sample_metadata.py +94 -0
  176. reader_workbench/plugins/validator/__init__.py +1 -0
  177. reader_workbench/plugins/validator/to_tidy_plus_map.py +155 -0
  178. reader_workbench/protocols/__init__.py +80 -0
  179. reader_workbench/protocols/_builtins_plate_reader_growth.py +179 -0
  180. reader_workbench/protocols/_builtins_plate_reader_variants.py +274 -0
  181. reader_workbench/protocols/builtins.py +1656 -0
  182. reader_workbench/protocols/compiler.py +22 -0
  183. reader_workbench/protocols/compilers/__init__.py +1 -0
  184. reader_workbench/protocols/compilers/common.py +100 -0
  185. reader_workbench/protocols/compilers/cytometry.py +87 -0
  186. reader_workbench/protocols/compilers/generic.py +14 -0
  187. reader_workbench/protocols/compilers/logic.py +245 -0
  188. reader_workbench/protocols/compilers/plate_reader.py +937 -0
  189. reader_workbench/protocols/compilers/plate_reader_pipeline.py +197 -0
  190. reader_workbench/protocols/model.py +1486 -0
  191. reader_workbench/protocols/semantic_coverage.py +234 -0
  192. reader_workbench/runtime/__init__.py +12 -0
  193. reader_workbench/runtime/builtin.py +23 -0
  194. reader_workbench/runtime/model.py +42 -0
  195. reader_workbench/workbench/__init__.py +60 -0
  196. reader_workbench/workbench/assets/__init__.py +22 -0
  197. reader_workbench/workbench/assets/types.py +118 -0
  198. reader_workbench/workbench/audit/__init__.py +5 -0
  199. reader_workbench/workbench/audit/experiments.py +307 -0
  200. reader_workbench/workbench/audit/staging.py +187 -0
  201. reader_workbench/workbench/cli/__init__.py +51 -0
  202. reader_workbench/workbench/cli/_lazy.py +9 -0
  203. reader_workbench/workbench/cli/_records_view.py +150 -0
  204. reader_workbench/workbench/cli/_surface_execution.py +443 -0
  205. reader_workbench/workbench/cli/audit.py +95 -0
  206. reader_workbench/workbench/cli/automation.py +229 -0
  207. reader_workbench/workbench/cli/demo.py +46 -0
  208. reader_workbench/workbench/cli/dop.py +91 -0
  209. reader_workbench/workbench/cli/experiments.py +635 -0
  210. reader_workbench/workbench/cli/helpers.py +232 -0
  211. reader_workbench/workbench/cli/main.py +59 -0
  212. reader_workbench/workbench/cli/maintenance.py +82 -0
  213. reader_workbench/workbench/cli/notebooks.py +260 -0
  214. reader_workbench/workbench/cli/pagination.py +117 -0
  215. reader_workbench/workbench/cli/protocols.py +336 -0
  216. reader_workbench/workbench/cli/shared.py +309 -0
  217. reader_workbench/workbench/cli/surfaces.py +534 -0
  218. reader_workbench/workbench/cli/verification.py +128 -0
  219. reader_workbench/workbench/commands.py +10 -0
  220. reader_workbench/workbench/config/__init__.py +47 -0
  221. reader_workbench/workbench/config/identity.py +13 -0
  222. reader_workbench/workbench/config/load.py +405 -0
  223. reader_workbench/workbench/config/model.py +274 -0
  224. reader_workbench/workbench/context.py +26 -0
  225. reader_workbench/workbench/decl/__init__.py +31 -0
  226. reader_workbench/workbench/decl/build.py +190 -0
  227. reader_workbench/workbench/decl/model.py +81 -0
  228. reader_workbench/workbench/dop/__init__.py +12 -0
  229. reader_workbench/workbench/dop/builtins.py +261 -0
  230. reader_workbench/workbench/dop/model.py +209 -0
  231. reader_workbench/workbench/engine/__init__.py +42 -0
  232. reader_workbench/workbench/engine/_shared.py +76 -0
  233. reader_workbench/workbench/engine/contracts.py +283 -0
  234. reader_workbench/workbench/engine/execution.py +326 -0
  235. reader_workbench/workbench/engine/file_outputs.py +260 -0
  236. reader_workbench/workbench/engine/inputs.py +161 -0
  237. reader_workbench/workbench/engine/invocations.py +507 -0
  238. reader_workbench/workbench/engine/planning.py +72 -0
  239. reader_workbench/workbench/engine/runtime.py +464 -0
  240. reader_workbench/workbench/engine/setup.py +149 -0
  241. reader_workbench/workbench/engine/validation.py +684 -0
  242. reader_workbench/workbench/experiment/__init__.py +47 -0
  243. reader_workbench/workbench/experiment/model.py +381 -0
  244. reader_workbench/workbench/experiments.py +133 -0
  245. reader_workbench/workbench/graph/__init__.py +47 -0
  246. reader_workbench/workbench/graph/nodes.py +102 -0
  247. reader_workbench/workbench/graph/normalize.py +177 -0
  248. reader_workbench/workbench/graph/refs.py +148 -0
  249. reader_workbench/workbench/input_discovery.py +19 -0
  250. reader_workbench/workbench/inspection/__init__.py +3 -0
  251. reader_workbench/workbench/inspection/catalogs.py +128 -0
  252. reader_workbench/workbench/inspection/common.py +92 -0
  253. reader_workbench/workbench/inspection/dop.py +64 -0
  254. reader_workbench/workbench/inspection/experiments.py +449 -0
  255. reader_workbench/workbench/inspection/inventory.py +68 -0
  256. reader_workbench/workbench/inspection/protocols.py +368 -0
  257. reader_workbench/workbench/inspection/readiness.py +333 -0
  258. reader_workbench/workbench/inspection/reports.py +367 -0
  259. reader_workbench/workbench/inspection/results.py +166 -0
  260. reader_workbench/workbench/inspection/runtime.py +287 -0
  261. reader_workbench/workbench/inspection/semantics.py +192 -0
  262. reader_workbench/workbench/inspection/validation.py +30 -0
  263. reader_workbench/workbench/notebooks/__init__.py +17 -0
  264. reader_workbench/workbench/notebooks/_launch_registry.py +112 -0
  265. reader_workbench/workbench/notebooks/_launch_runtime.py +104 -0
  266. reader_workbench/workbench/notebooks/components/__init__.py +21 -0
  267. reader_workbench/workbench/notebooks/components/deliverables.py +403 -0
  268. reader_workbench/workbench/notebooks/components/overview.py +119 -0
  269. reader_workbench/workbench/notebooks/eda.marimo.py.txt +153 -0
  270. reader_workbench/workbench/notebooks/launch.py +274 -0
  271. reader_workbench/workbench/notebooks/presentation.py +136 -0
  272. reader_workbench/workbench/notebooks/scaffold.py +60 -0
  273. reader_workbench/workbench/ontology.py +78 -0
  274. reader_workbench/workbench/paths.py +44 -0
  275. reader_workbench/workbench/ports/__init__.py +31 -0
  276. reader_workbench/workbench/ports/model.py +168 -0
  277. reader_workbench/workbench/records/__init__.py +44 -0
  278. reader_workbench/workbench/records/epoch.py +329 -0
  279. reader_workbench/workbench/records/evidence.py +247 -0
  280. reader_workbench/workbench/records/identity.py +87 -0
  281. reader_workbench/workbench/records/locking.py +185 -0
  282. reader_workbench/workbench/records/model.py +711 -0
  283. reader_workbench/workbench/records/sources.py +73 -0
  284. reader_workbench/workbench/records/store.py +1022 -0
  285. reader_workbench/workbench/records/verification.py +998 -0
  286. reader_workbench/workbench/registry.py +333 -0
  287. reader_workbench/workbench/spec_overrides.py +215 -0
  288. reader_workbench-1.0.0.dist-info/METADATA +91 -0
  289. reader_workbench-1.0.0.dist-info/RECORD +293 -0
  290. reader_workbench-1.0.0.dist-info/WHEEL +5 -0
  291. reader_workbench-1.0.0.dist-info/entry_points.txt +2 -0
  292. reader_workbench-1.0.0.dist-info/licenses/LICENSE +21 -0
  293. 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
+ }